In this comprehensive guide, you’ll learn how to implement JWT (JSON Web Token) authentication in a Golang application using GORM and the Fiber web framework. The REST API will be powered by a high-performance Fiber HTTP server, offering endpoints dedicated to secure user authentication, and persist data in a PostgreSQL database.

What is JWT? JSON Web Token, also commonly referred to as JWT, is an open standard for securely transmitting information between a client and a server, or between two servers.

For example, the client (such as a browser or mobile app) requests an access token from the server, and the server returns a JWT to the client. The client then sends the JWT in subsequent requests to access protected resources on the server. The server extracts the information contained in the token to validate the user’s identity and determine their access to the resources.

More practice:

Golang, GORM, & Fiber JWT Authentication

Prerequisites

For an optimal understanding of the knowledge presented in this article, it is recommended to have the following prerequisites:

  • Make sure to have Docker installed on your device, as it is a requirement to launch the Postgres and pgAdmin containers.
  • Knowledge of Golang and its ecosystem. A strong understanding of Golang syntax and concepts will be beneficial.
  • Knowledge of GORM is necessary for storing and retrieving data in the Postgres database.

Run the Golang & Fiber JWT Auth Project

  • To access the Golang Fiber JWT Authentication project, either download it from the following link: https://github.com/wpcodevo/golang-fiber-jwt or clone it using Git. After obtaining the source code, open it in a code editor of your choice.
  • Open the integrated terminal in your IDE and execute the command docker-compose up -d to launch the Postgres and pgAdmin servers in the Docker containers.
  • Then, run the command go install to compile and install the project’s dependencies.
  • Start the Fiber HTTP server and migrate the GORM model to the Postgres database by running go run main.go.
  • To test the authentication flow of the Fiber API, import the file Golang_GORM.postman_collection.json into either Postman or the Thunder client VS Code extension.

Setup the Golang Project

Upon completion of this tutorial, the folder structure of your project should resemble the screenshot below, with the exception of the Makefile and “Golang_GORM.postman_collection.json” files.

folder structure of the golang fiber jwt authentication project

To begin, navigate to your desktop or any convenient location on your machine and create a folder with the name golang-fiber-jwt . Feel free to use a different name for the folder.

Next, navigate to the newly-created folder and initialize the Golang project by executing the command go mod init. Don’t forget to replace wpcodevo with your GitHub username.


mkdir golang-fiber-jwt
cd golang-fiber-jwt
go mod init github.com/wpcodevo/golang-fiber-jwt

Once the Golang project is initialized, run the following commands to install the required packages and dependencies for the project.


go get github.com/gofiber/fiber/v2
go get github.com/google/uuid
go get github.com/go-playground/validator/v10
go get -u gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/spf13/viper
go get github.com/golang-jwt/jwt
# Hot-reload server
go install github.com/cosmtrek/air@latest

  • fiber – A fast and flexible HTTP network framework for Golang.
  • uuid – A package to generate and parse UUIDs in Golang.
  • validator – This package provides an interface for validating input data of a Golang application.
  • gorm – This package provides a simple and intuitive way to interact with relational databases, such as MySQL, PostgreSQL, and SQLite, using Go structs as models.
  • postgres – A Postgres database driver for GORM. This Postgres driver is compatible with GORM’s API and supports all of its features, including creating, updating, and retrieving data, transactions, migrations, and more.
  • viper – This package provides a convenient way to read and access configuration data from various sources, such as environment variables, command-line arguments, configuration files, and even remote key-value stores.
  • jwt – A popular Golang library for handling JSON Web Tokens. It supports a wide range of JWT standards, including signing algorithms such as HMAC-SHA256, RSA, and ECDSA, as well as JWT claims such as standard and custom claims.
  • air – This Golang utility allows developers to reload the application automatically whenever a change is made to the source code, without having to manually restart the server.

After the dependencies have been installed, open the project in your preferred IDE or text editor.

To get our hands dirty, let’s create a basic Fiber HTTP server to respond with a simple JSON object when a GET request is made to the health checker route.

In the root directory, create a main.go file and add the following code:

main.go


package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
)

func main() {
	app := fiber.New()

	app.Use(logger.New())

	app.Get("/api/healthchecker", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"status":  "success",
			"message": "Welcome to Golang, Fiber, and GORM",
		})
	})

	log.Fatal(app.Listen(":8000"))
}

In the above code, we created a new Fiber app and assigned it to the app variable. Then, we added a GET route at /api/healthchecker to the middleware stack. When a request is made to the route, it will evoke the Fiber context handler to return the JSON object.

Finally, we called the app.Listen(":8000") method to start the app and listen on port 8000. To see the JSON object, make a GET request to thehttp://localhost:8000/api/healthchecker URL in an API client or the browser.

testing the health checker route of the golang jwt authentication api

Setup PostgreSQL and pgAdmin with Docker

In this section, we’ll utilize Docker to launch PostgreSQL and pgAdmin, instead of downloading them from their official websites. Using Docker to set up Postgres and pgAdmin offers several benefits over downloading them directly from their official websites. Some of the benefits include:

  1. Reproducibility: Docker containers are easily reproducible, meaning that the same environment can be recreated on any machine that has Docker installed.
  2. Portability: Docker containers can be easily moved from one machine to another, which makes it easy to develop an application on one machine and then deploy it to another.
  3. Ease of Setup: Docker allows developers to quickly and easily set up complex environments like PostgreSQL and pgAdmin, which can be difficult to set up and configure manually.

With the above explanation, create a docker-compose.yml file in the root directory and add the following Docker compose configurations.

docker-compose.yml


version: '3'
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    ports:
      - '6500:5432'
    volumes:
      - progresDB:/data/postgres
    env_file:
      - ./app.env
  pgAdmin:
    image: dpage/pgadmin4
    container_name: pgAdmin
    env_file:
      - ./app.env
    ports:
      - "5050:80"
volumes:
  progresDB:

To make the Postgres and pgAdmin credentials available to Docker compose, let’s create an app.env file, which we referenced with the env_file property in the docker-compose.yml file.

Create an app.env file in the root directory and add the following environment variables.

app.env


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=golang_fiber_jwt

DATABASE_URL=postgresql://admin:password123@localhost:6500/golang_fiber?schema=public

CLIENT_ORIGIN=http://localhost:3000

PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123

JWT_SECRET=my_ultra_secure_secret
JWT_EXPIRED_IN=60m
JWT_MAXAGE=60

With the above configurations, run the command docker-compose up -d to launch Postgres and pgAdmin with Docker. Open the Docker Desktop application to see the running containers.

inspect the running postgres and pgAdmin containers

Create the GORM Model

With the Postgres server up and running in the Docker container, let’s create a GORM database model to represent the structure of the SQL table. To do this, we’ll create a Go struct that will serve as a blueprint for GORM to generate the corresponding SQL table in the database.

The Go struct represents the underlying SQL table, whereas the fields represent the columns of the table. Create a models folder in the root directory and inside it, create a file named user.model.go and add the following code.

models/user.model.go


package models

import (
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/google/uuid"
)

type User struct {
	ID        *uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primary_key"`
	Name      string     `gorm:"type:varchar(100);not null"`
	Email     string     `gorm:"type:varchar(100);uniqueIndex;not null"`
	Password  string     `gorm:"type:varchar(100);not null"`
	Role      *string    `gorm:"type:varchar(50);default:'user';not null"`
	Provider  *string    `gorm:"type:varchar(50);default:'local';not null"`
	Photo     *string    `gorm:"not null;default:'default.png'"`
	Verified  *bool      `gorm:"not null;default:false"`
	CreatedAt *time.Time `gorm:"not null;default:now()"`
	UpdatedAt *time.Time `gorm:"not null;default:now()"`
}

type SignUpInput struct {
	Name            string `json:"name" validate:"required"`
	Email           string `json:"email" validate:"required"`
	Password        string `json:"password" validate:"required,min=8"`
	PasswordConfirm string `json:"passwordConfirm" validate:"required,min=8"`
	Photo           string `json:"photo"`
}

type SignInInput struct {
	Email    string `json:"email"  validate:"required"`
	Password string `json:"password"  validate:"required"`
}

type UserResponse struct {
	ID        uuid.UUID `json:"id,omitempty"`
	Name      string    `json:"name,omitempty"`
	Email     string    `json:"email,omitempty"`
	Role      string    `json:"role,omitempty"`
	Photo     string    `json:"photo,omitempty"`
	Provider  string    `json:"provider"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

func FilterUserRecord(user *User) UserResponse {
	return UserResponse{
		ID:        *user.ID,
		Name:      user.Name,
		Email:     user.Email,
		Role:      *user.Role,
		Photo:     *user.Photo,
		Provider:  *user.Provider,
		CreatedAt: *user.CreatedAt,
		UpdatedAt: *user.UpdatedAt,
	}
}

var validate = validator.New()

type ErrorResponse struct {
	Field string `json:"field"`
	Tag   string `json:"tag"`
	Value string `json:"value,omitempty"`
}

func ValidateStruct[T any](payload T) []*ErrorResponse {
	var errors []*ErrorResponse
	err := validate.Struct(payload)
	if err != nil {
		for _, err := range err.(validator.ValidationErrors) {
			var element ErrorResponse
			element.Field = err.StructNamespace()
			element.Tag = err.Tag()
			element.Value = err.Param()
			errors = append(errors, &element)
		}
	}
	return errors
}

The SignUpInput struct represents the structure of the signup request body, whereas, the SignInInput struct represents the structure of the sign-in request body.

The ValidateStruct function will be used to validate the request body against a specified validation schema. If the request body does not match the schema, the function will return relevant validation errors to the client.

For simplicity, we’ll keep the “SignUpInput“, “SignInInput“, and “UserResponse” structs in the models/user.model.go file, rather than placing them in a separate schemas/user.schema.go file.

Database Migration with GORM

At this point, we are ready to create a connection pool between the Postgres server and the Go application. Once the database pool has been established, GORM will migrate the model to the database and keep the model in sync with the database schema.

Load the Environment Variables with Viper

Let’s start by creating a function that utilizes the Viper package to load environment variables into the Golang runtime. Create an initializers folder in the root directory and inside it, create a file named loadEnv.go and add the code below.

initializers/loadEnv.go


package initializers

import (
	"time"

	"github.com/spf13/viper"
)

type Config struct {
	DBHost         string `mapstructure:"POSTGRES_HOST"`
	DBUserName     string `mapstructure:"POSTGRES_USER"`
	DBUserPassword string `mapstructure:"POSTGRES_PASSWORD"`
	DBName         string `mapstructure:"POSTGRES_DB"`
	DBPort         string `mapstructure:"POSTGRES_PORT"`

	JwtSecret    string        `mapstructure:"JWT_SECRET"`
	JwtExpiresIn time.Duration `mapstructure:"JWT_EXPIRED_IN"`
	JwtMaxAge    int           `mapstructure:"JWT_MAXAGE"`

	ClientOrigin string `mapstructure:"CLIENT_ORIGIN"`
}

func LoadConfig(path string) (config Config, err error) {
	viper.AddConfigPath(path)
	viper.SetConfigType("env")
	viper.SetConfigName("app")

	viper.AutomaticEnv()

	err = viper.ReadInConfig()
	if err != nil {
		return
	}

	err = viper.Unmarshal(&config)
	return
}

The LoadConfig function will retrieve the environment variables from the app.env file, then unmarshal the contents into the Config struct.

Create the Database Pool with GORM

Let’s now create a function that we will later invoke to establish a connection between the Go application and the Postgres server, and migrate the GORM model to the database.

To enable the use of the uuid-ossp extension, the function will use GORM’s DB.Exec() method to execute a raw SQL statement and install the plugin if it’s not already installed.

Since we have utilized the uuid_generate_v4() function of the uuid-ossp contrib module as the default value for the ID column, it needs to be installed.

initializers/db.go


package initializers

import (
	"fmt"
	"log"
	"os"

	"github.com/wpcodevo/golang-fiber-jwt/models"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

var DB *gorm.DB

func ConnectDB(config *Config) {
	var err error
	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", config.DBHost, config.DBUserName, config.DBUserPassword, config.DBName, config.DBPort)

	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to the Database! \n", err.Error())
		os.Exit(1)
	}

	DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
	DB.Logger = logger.Default.LogMode(logger.Info)

	log.Println("Running Migrations")
	err = DB.AutoMigrate(&models.User{})
	if err != nil {
		log.Fatal("Migration Failed:  \n", err.Error())
		os.Exit(1)
	}

	log.Println("🚀 Connected Successfully to the Database")
}

Migrate the GORM Model to the Database

Now let’s create the init() function to evoke the LoadConfig() and ConnectDB() functions. Replace the content of the main.go file with the code below.

main.go


package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/wpcodevo/golang-fiber-jwt/initializers"
)

func init() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatalln("Failed to load environment variables! \n", err.Error())
	}
	initializers.ConnectDB(&config)
}

func main() {
	app := fiber.New()

	app.Use(logger.New())

	app.Get("/api/healthchecker", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"status":  "success",
			"message": "Welcome to Golang, Fiber, and GORM",
		})
	})

	log.Fatal(app.Listen(":8000"))
}

Compile and run the Golang application by running go run main.go or air. This will initiate the following actions:

  1. The environment variables will be retrieved from the app.env file and loaded into the Golang runtime.
  2. A connection pool will be created between the Go application and the Postgres server.
  3. The installation of the uuid-ossp extension and the insertion of an SQL table named “users” into the database will take place.
  4. The Fiber HTTP server will be started.

Now let’s use pgAdmin to verify if the SQL table was inserted by GORM. First, open http://localhost:5050/ to access the pgAdmin container and sign in with the credentials specified in the app.env file.

Next, we need to get the IP address of the Postgres container. Run the command docker inspect postgres to print the information about the container in the terminal.

Navigate to the “Networks” section and copy the “IPAddress” property value. Then, add a new server using the Postgres credentials and use the copied IP address as the Host name/address.

register the postgres server in pgadmin

Upon connecting pgAdmin to the Postgres server, inspect the golang_fiber_jwt database to view the “users” table.

inspect the sql table added by gorm

Create the JWT Authentication Controllers

At this point, we have covered a lot of code and are now ready to implement the JWT authentication flow using Fiber context handlers. These route functions will handle user registration, sign-in, and logout for the API.

SignUp User Fiber Context Handler

This route handler is responsible for processing user registration requests, validating the incoming data, storing the user’s information in the database, and returning a filtered version of the user’s record in the JSON response.

When a POST request is made to the /api/auth/register endpoint, the Fiber framework will trigger this route function to parse and validate the incoming data according to the validation rules specified in the models.ValidateStruct() struct.

Next, the function will check if the Password and PasswordConfirm fields match. Upon successful validation, it will proceed to hash the password for security and utilize GORM’s DB.Create() method to store the new user’s information in the database.

In the event that a user with the same email already exists, a 409 Conflict error will be returned to the client. On the other hand, if the registration is successful, a filtered version of the newly registered user will be returned in the JSON response.

controllers/auth.controller.go


func SignUpUser(c *fiber.Ctx) error {
	var payload *models.SignUpInput

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	errors := models.ValidateStruct(payload)
	if errors != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "errors": errors})

	}

	if payload.Password != payload.PasswordConfirm {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Passwords do not match"})

	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(payload.Password), bcrypt.DefaultCost)

	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	newUser := models.User{
		Name:     payload.Name,
		Email:    strings.ToLower(payload.Email),
		Password: string(hashedPassword),
		Photo:    &payload.Photo,
	}

	result := initializers.DB.Create(&newUser)

	if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {
		return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "User with that email already exists"})
	} else if result.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": "Something bad happened"})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"user": models.FilterUserRecord(&newUser)}})
}

SignIn User Fiber Context Handler

This Fiber context handler will be responsible for processing user sign-in requests. It will be triggered when a POST request is sent to the /api/auth/login endpoint.

When this Fiber context handler is triggered, it will parse the incoming data, validate the fields according to the models.SignInInput struct, and check the database to see if a user with the provided email exists.

If the user exists, the handler will compare the provided password with the one stored in the database by utilizing the bcrypt.CompareHashAndPassword() method. This will confirm if the user has provided the correct credentials.

Upon successful authentication, a JWT token will be generated using the github.com/golang-jwt/jwt package and returned to the client in the form of an HTTP-only cookie.

controllers/auth.controller.go


func SignInUser(c *fiber.Ctx) error {
	var payload *models.SignInInput

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	errors := models.ValidateStruct(payload)
	if errors != nil {
		return c.Status(fiber.StatusBadRequest).JSON(errors)

	}

	var user models.User
	result := initializers.DB.First(&user, "email = ?", strings.ToLower(payload.Email))
	if result.Error != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Invalid email or Password"})
	}

	err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(payload.Password))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Invalid email or Password"})
	}

	config, _ := initializers.LoadConfig(".")

	tokenByte := jwt.New(jwt.SigningMethodHS256)

	now := time.Now().UTC()
	claims := tokenByte.Claims.(jwt.MapClaims)

	claims["sub"] = user.ID
	claims["exp"] = now.Add(config.JwtExpiresIn).Unix()
	claims["iat"] = now.Unix()
	claims["nbf"] = now.Unix()

	tokenString, err := tokenByte.SignedString([]byte(config.JwtSecret))

	if err != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": fmt.Sprintf("generating JWT Token failed: %v", err)})
	}

	c.Cookie(&fiber.Cookie{
		Name:     "token",
		Value:    tokenString,
		Path:     "/",
		MaxAge:   config.JwtMaxAge * 60,
		Secure:   false,
		HTTPOnly: true,
		Domain:   "localhost",
	})

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "token": tokenString})
}

Logout User Fiber Context Handler

Finally, let’s create a Fiber context handler to process sign-out requests. This route handler will be evoked when a GET request is made to the /api/auth/logout endpoint.

To sign out a user, an expired cookie will be sent, effectively deleting the current cookie stored in their API client or web browser.

controllers/auth.controller.go


func LogoutUser(c *fiber.Ctx) error {
	expired := time.Now().Add(-time.Hour * 24)
	c.Cookie(&fiber.Cookie{
		Name:    "token",
		Value:   "",
		Expires: expired,
	})
	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success"})
}

Complete Fiber Context Handlers

controllers/auth.controller.go


package controllers

import (
	"fmt"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt"
	"github.com/wpcodevo/golang-fiber-jwt/initializers"
	"github.com/wpcodevo/golang-fiber-jwt/models"
	"golang.org/x/crypto/bcrypt"
)

func SignUpUser(c *fiber.Ctx) error {
	var payload *models.SignUpInput

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	errors := models.ValidateStruct(payload)
	if errors != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "errors": errors})

	}

	if payload.Password != payload.PasswordConfirm {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Passwords do not match"})

	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(payload.Password), bcrypt.DefaultCost)

	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	newUser := models.User{
		Name:     payload.Name,
		Email:    strings.ToLower(payload.Email),
		Password: string(hashedPassword),
		Photo:    &payload.Photo,
	}

	result := initializers.DB.Create(&newUser)

	if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {
		return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "User with that email already exists"})
	} else if result.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": "Something bad happened"})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"user": models.FilterUserRecord(&newUser)}})
}

func SignInUser(c *fiber.Ctx) error {
	var payload *models.SignInInput

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	errors := models.ValidateStruct(payload)
	if errors != nil {
		return c.Status(fiber.StatusBadRequest).JSON(errors)

	}

	var user models.User
	result := initializers.DB.First(&user, "email = ?", strings.ToLower(payload.Email))
	if result.Error != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Invalid email or Password"})
	}

	err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(payload.Password))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Invalid email or Password"})
	}

	config, _ := initializers.LoadConfig(".")

	tokenByte := jwt.New(jwt.SigningMethodHS256)

	now := time.Now().UTC()
	claims := tokenByte.Claims.(jwt.MapClaims)

	claims["sub"] = user.ID
	claims["exp"] = now.Add(config.JwtExpiresIn).Unix()
	claims["iat"] = now.Unix()
	claims["nbf"] = now.Unix()

	tokenString, err := tokenByte.SignedString([]byte(config.JwtSecret))

	if err != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": fmt.Sprintf("generating JWT Token failed: %v", err)})
	}

	c.Cookie(&fiber.Cookie{
		Name:     "token",
		Value:    tokenString,
		Path:     "/",
		MaxAge:   config.JwtMaxAge * 60,
		Secure:   false,
		HTTPOnly: true,
		Domain:   "localhost",
	})

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "token": tokenString})
}

func LogoutUser(c *fiber.Ctx) error {
	expired := time.Now().Add(-time.Hour * 24)
	c.Cookie(&fiber.Cookie{
		Name:    "token",
		Value:   "",
		Expires: expired,
	})
	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success"})
}

Get the Authenticated User

Access to this Fiber context handler will be restricted to users with valid JWTs, enforced by a JWT middleware guard. Don’t worry, we’ll create the authentication guard shortly.

When this route function is evoked, it will extract the user’s credentials from Fiber’s Locals storage and send them back to the client in the JSON response.

controllers/user.controller.go


package controllers

import (
	"github.com/gofiber/fiber/v2"
	"github.com/wpcodevo/golang-fiber-jwt/models"
)

func GetMe(c *fiber.Ctx) error {
	user := c.Locals("user").(models.UserResponse)

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"user": user}})
}

Create the JWT Middleware Guard

Now let’s create the JWT middleware guard to validate the token and allow access to protected resources. We’ll design the middleware to extract the JWT token from either the Authorization header or the Cookies object, making it more adaptable.

Create a deserialize-user.go file within a “middleware” folder in the root directory, and add the following code.

middleware/deserialize-user.go


package middleware

import (
	"fmt"
	"strings"

	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt"
	"github.com/wpcodevo/golang-fiber-jwt/initializers"
	"github.com/wpcodevo/golang-fiber-jwt/models"
)

func DeserializeUser(c *fiber.Ctx) error {
	var tokenString string
	authorization := c.Get("Authorization")

	if strings.HasPrefix(authorization, "Bearer ") {
		tokenString = strings.TrimPrefix(authorization, "Bearer ")
	} else if c.Cookies("token") != "" {
		tokenString = c.Cookies("token")
	}

	if tokenString == "" {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "fail", "message": "You are not logged in"})
	}

	config, _ := initializers.LoadConfig(".")

	tokenByte, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
		if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %s", jwtToken.Header["alg"])
		}

		return []byte(config.JwtSecret), nil
	})
	if err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "fail", "message": fmt.Sprintf("invalidate token: %v", err)})
	}

	claims, ok := tokenByte.Claims.(jwt.MapClaims)
	if !ok || !tokenByte.Valid {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "fail", "message": "invalid token claim"})

	}

	var user models.User
	initializers.DB.First(&user, "id = ?", fmt.Sprint(claims["sub"]))

	if user.ID.String() != claims["sub"] {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"status": "fail", "message": "the user belonging to this token no logger exists"})
	}

	c.Locals("user", models.FilterUserRecord(&user))

	return c.Next()
}

The DeserializeUser function will attempt to retrieve the JWT token, first by checking the Authorization header, and then by searching for it in the Cookies object if it is not present in the header.

If the JWT token is not found in either the Authorization header or the Cookies object, the client will receive a 401 Unauthorized error. However, If the JWT token is present, it will be verified using the github.com/golang-jwt/jwt package to extract its claims.

Subsequently, GORM’s DB.First() method will be utilized to confirm the existence of the user associated with the token in the database. In the event that the user is found, their profile information retrieved from the query will be stored in Fiber’s Locals storage.

Upon successful completion of the verification steps, the request will be delegated to the subsequent middleware.

Register the Routes and Add CORS

Now that we’ve completed the authentication aspect of the API, we can now proceed to create routes for the Fiber context handlers and incorporate CORS into the server.

Adding CORS to the Fiber server will allow it to receive and respond to cross-domain requests, such as AJAX requests, made from a frontend application hosted on a different domain.

To achieve this, open the main.go file and replace its content with the following code.

main.go


package main

import (
	"fmt"
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/wpcodevo/golang-fiber-jwt/controllers"
	"github.com/wpcodevo/golang-fiber-jwt/initializers"
	"github.com/wpcodevo/golang-fiber-jwt/middleware"
)

func init() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatalln("Failed to load environment variables! \n", err.Error())
	}
	initializers.ConnectDB(&config)
}

func main() {
	app := fiber.New()
	micro := fiber.New()

	app.Mount("/api", micro)
	app.Use(logger.New())
	app.Use(cors.New(cors.Config{
		AllowOrigins:     "http://localhost:3000",
		AllowHeaders:     "Origin, Content-Type, Accept",
		AllowMethods:     "GET, POST",
		AllowCredentials: true,
	}))

	micro.Route("/auth", func(router fiber.Router) {
		router.Post("/register", controllers.SignUpUser)
		router.Post("/login", controllers.SignInUser)
		router.Get("/logout", middleware.DeserializeUser, controllers.LogoutUser)
	})

	micro.Get("/users/me", middleware.DeserializeUser, controllers.GetMe)

	micro.Get("/healthchecker", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).JSON(fiber.Map{
			"status":  "success",
			"message": "Welcome to Golang, Fiber, and GORM",
		})
	})

	micro.All("*", func(c *fiber.Ctx) error {
		path := c.Path()
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"status":  "fail",
			"message": fmt.Sprintf("Path: %v does not exists on this server", path),
		})
	})

	log.Fatal(app.Listen(":8000"))
}

And we are done! You can now start the Fiber HTTP server again by running either go run main.go or air.

Testing the JWT Authentication Flow

Now that the server is listening on port 8000 and ready to receive requests, let’s test the JWT authentication process by making HTTP requests. To access the API collection used in testing the endpoints, clone the project, and import the Golang_GORM.postman_collection.json file into Postman or Thunder Client VS Code extension.

Register a New Account

To create a new account, add the required credentials to the request body as a JSON object and send a POST request tohttp://localhost:8000/api/auth/register.


{
  "email": "admin@admin.com",
  "name": "Admin",
  "password": "password123",
  "passwordConfirm": "password123"
}

To ensure the request body is recognized as JSON, add a Content-Type: application/json header to the API client request if the option to select JSON is not available.

Golang, GORM, and Fiber JWT Authentication Register new user

The Fiber server will delegate the request to the SignUpUser route handler, which will insert the new user into the database and return the newly-created record in the JSON response.

Golang, GORM, and Fiber JWT Authentication Register new user response

Log into the Account

To sign into your account, add the required credentials (email and password) to the request body as a JSON object and send a POST request tohttp://localhost:8000/api/auth/login.


{
  "email": "admin@admin.com",
  "password": "password123"
}

Golang, GORM, and Fiber JWT Authentication Login

Once the Fiber server receives the request, it will forward it to the SignInUser context handler. The SignInUser route function will validate the credentials, generate a JWT token, and return the token to the API client.

The token will be available as an HTTP-only cookie and in the JSON response. This way, you can include it in subsequent requests as a Bearer token or Cookie.

Golang, GORM, and Fiber JWT Authentication Login response

Access Protected Routes

To retrieve your account information, send a GET request to http://localhost:8000/api/users/me . Since this route is protected, you need to add the JWT token as Bearer in the Authorization header if your API client can’t send cookies.

Golang, GORM, and Fiber JWT Authentication Get Profile Information

When the Fiber server receives the request, it will forward it to the DeserializeUser middleware guard, which will extract the token, validate it, and delegate the request to the GetMe route function if the token is valid.

The GetMe route handler will then extract the user data from Fiber’s Locals storage and return the data in the JSON response.

Golang, GORM, and Fiber JWT Authentication Get Profile Information response

Logout from the API

To logout, add the JWT token to the Authorization header as a Bearer token and send a GET request to http://localhost:8000/api/auth/logout.

Golang, GORM, and Fiber JWT Authentication Logout

Conclusion

In this article, you learned how to implement JSON Web Token authentication in a Golang application using GORM and the Fiber web framework. The API encompasses all the necessary authentication features, including registering new users, logging them in, and logging them out.

We even went a step further to create an authentication guard to ensure a valid JWT token is provided before granting access to protected resources.

I hope this article was helpful and informative. Please leave a comment if you have any questions. Thank you for taking the time to read it.

You can find the complete source code of the Golang Fiber JWT project on GitHub.