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 & MongoDB: JWT Authentication and Authorization
- API with Golang + MongoDB: Send HTML Emails with Gomail
- Golang, SQLC, and PostgreSQL: JWT Access & Refresh Tokens
- API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
- Build Golang gRPC Server and Client: SignUp User & Verify Email
- Build Golang gRPC Server and Client: Access & Refresh Tokens
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.
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.
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:
- Reproducibility: Docker containers are easily reproducible, meaning that the same environment can be recreated on any machine that has Docker installed.
- 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.
- 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.
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:
- The environment variables will be retrieved from the
app.env
file and loaded into the Golang runtime. - A connection pool will be created between the Go application and the Postgres server.
- The installation of the
uuid-ossp
extension and the insertion of an SQL table named “users” into the database will take place. - 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.
Upon connecting pgAdmin to the Postgres server, inspect the golang_fiber_jwt
database to view the “users” table.
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.
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.
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"
}
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.
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.
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.
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
.
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.