How to Implement JWT Authentication in Go  - I

This blog post provides a comprehensive guide on implementing JWT authentication in a Go web application. It covers various aspects of the implementation process, from setting up the project, installing required packages to implementing JWT middleware using the Gin Gonic framework.

GraphQL has a role beyond API Query Language- being the backbone of application Integration
background Coditation

How to Implement JWT Authentication in Go  - I

What is  JWT authentication

JWT (short for JSON Web Token) authentication is a popular method for securing web applications and APIs. A JWT is a digitally signed token that contains a set of claims or statements about a user, such as their identity or authorization level. When a user logs in to a web application, the server generates a JWT and sends it back to the client, storing it in a cookie or local storage. The client then sends the JWT with each subsequent request to the server, which verifies the token and uses the user data in the token’s claims to authorize the request.

Why use JWT in Go

JWT is a widely used authentication standard supported by many programming languages and frameworks. Using JWT in Go can simplify authentication and provide a secure way to transmit user data between the client and server. Go’s strong typing and support for concurrency make it well-suited for building scalable and secure web applications, and using JWT can be a useful tool in this regard.

Overview of the blog

In this blog, we will explore how to implement JWT authentication in a Go web application. We will start by setting up the project and installing the required packages. Then, we will look at how to generate a JWT token and how to authenticate it. We will also implement a middleware to protect our routes, and look at how to store the JWT in a cookie for added security. By the end of this blog, you should have a good understanding of how to use JWT authentication in your Go web applications.

Installing Go and dependencies

Before we can begin implementing JWT authentication in our Go web application, we first need to make sure that Go is installed on our system. You can download and install the latest version of Go from the official Go website: golang.org/dl. You can follow our guide on how to install and set up Go: https://rekib-ahmed023.hashnode.dev/installing-and-setting-up-go-for-development

Once Go is installed, we can create a new project directory and initialize it as a Go module. We can do this by running the following commands in our terminal:
mkdir go-jwt-authentication
cd go-jwt-authentication
go mod init go-jwt-authentication
This will create a new directory called go-jwt-authentication, and initialize it as a Go module with the module path go-jwt-authentication.
We will also need to install some additional packages to work with JWT in Go. The two main packages we will be using are jwt-go for generating and verifying JWT tokens, and gin for handling HTTP requests and routing. We can install these packages using the go get command:
go get github.com/dgrijalva/jwt-go
go get github.com/gin-gonic/gin

Creating a new project

Now that we have our project directory set up and our dependencies installed, we can start creating our Go web application. We can create a new file called main.go in the root directory of our project, and start by importing the necessary packages:


package main

import (
   "fmt"
   "net/http"
   "github.com/dgrijalva/jwt-go"
   "github.com/gin-gonic/gin"
)

Adding required packages

Next, we can define our main function and start configuring our HTTP server using the gin package:
To start the project, run the following command:
go run main.go
We will come back to defining our routes in later sections of the blog. For now, we have successfully set up our Go project and installed the necessary packages to start implementing JWT authentication in our web application.

Project Structure

When building any Go project, it’s important to follow a well-organized directory structure to maintain code readability, scalability, and maintainability. Here’s an example project structure we’ll be following for our JWT authentication implementation:

├── controllers

│   ├── authController.go

│   └── userController.go

├── database

│   └── connection.go

├── helpers

│   ├── config.go

│   ├── cookieHelper.go

│   └── tokenHelper.go

├── middleware

│   └── authMiddleware.go

├── models

│   └── userModel.go

├── routes

│   ├── authRouter.go

│   └── userRouter.go

├── serializers

│   └── serializers.go

├── go.mod

├── go.sum

├── main.go

└── Makefile

This project structure organizes the code by functionality, separating the database connection code from the controllers and middleware code. The routes directory contains the routing configuration for the API endpoints, while the models directory contains the data models used in the application. The helpers directory contains utility functions for handling JWT tokens and cookies, while the serializers directory contains functions for serializing and deserializing data between the application and the client.

Creating User Model

In this section, we will create a User model using Go's gorm package. The User model will define the fields that we want to store in our database for each user. We'll be using the gorm.Model struct, which contains the default fields of id, created_at, updated_at, and deleted_at.
Our User model will also include the following fields:

  • FirstName (string): The first name of the user.
  • LastName (string): The last name of the user.
  • Email (string): The email address of the user. We'll also add a unique constraint to ensure that there are no duplicate email addresses in the database.
  • Password (string): The hashed password of the user.

Here’s what the User model implementation might look like:


package models

import (
   "gorm.io/gorm"
)

type User struct {
   gorm.Model
   FirstName string `json:"first_name" validate:"required"`
   LastName  string `json:"last_name" validate:"required"`
   Email     string `gorm:"unique" json:"email" validate:"required,email"`
   Password  string `json:"password" validate:"required"`
}

In this implementation, we’ve defined the fields using struct tags, which are used to add metadata to the struct fields. We’ve added the json tag to specify the JSON key for each field, and the validate tag to add validation rules for each field. We've also added the gorm tag to specify the database constraints for the Email field.
By defining our model using struct tags, we can easily serialize and deserialize our model to and from JSON, and we can also easily validate the incoming data before storing it in our database.

Connecting to the Database

Once we have our project structure set up and our user model ready, we can connect to our database using Go’s gorm package. We will create a connection.go file in the database directory, which will handle the database connection and provide a DB object that we can use throughout the application.
Here, we will be using Docker to run our Postgres container. First, make sure that Docker is installed on your system. You can download and install Docker from the official Docker website: docker.com/get-started.
Once Docker is installed, we can create a new container for our Postgres database using the following command:

docker run --name postgres15 -p 5433:5432 -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypassword -d postgres:15-alpine

To simplify the setup process, we can create a Makefile that includes the necessary commands to run the PostgreSQL container and create a new database. This way, we can quickly and easily set up our database without having to manually execute each command.

Here is an example of what the Makefile might look like:


ENV := $(PWD)/.env

include $(ENV)

postgresinit:
   docker run --name postgres15 -p $(DB_PORT):5432 -e POSTGRES_USER=$(DB_USER) -e POSTGRES_PASSWORD=$(DB_PASSWORD) -d postgres:15-alpine

postgres:
   docker exec -it postgres15 psql

createdb:
   docker exec -it postgres15 createdb --username=$(DB_USER) --owner=$(DB_USER) $(DB_NAME)

dropdb:
   docker exec -it postgres15 dropdb $(DB_NAME)

.PHONY: postgresinit postgres createdb dropdb

Next, we can create a connection.go file in the database directory with the following code:


package database

import (
   "fmt"
   "log"

   "go-jwt-authentication/helpers"
   "go-jwt-authentication/models"

   "gorm.io/driver/postgres"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
)

type DbInstance struct {
   Db *gorm.DB
}

var Database DbInstance

func Connect() {
   dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", helpers.AppConfig.DB_HOST, helpers.AppConfig.DB_USER, helpers.AppConfig.DB_PASSWORD, helpers.AppConfig.DB_USER, helpers.AppConfig.DB_PORT)
   fmt.Println(dsn)
   db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

   if err != nil {
       panic("Could not connect to the database")
   }
log.Println("Connected to the database successfully")

   db.Logger = logger.Default.LogMode(logger.Info)

   log.Println("Running Migrations")
   db.AutoMigrate(&models.User{})

   Database = DbInstance{Db: db}
}

Before explaining the code above, let’s create a helpers/config.go file that will contain the necessary environment variables mapping used in our application.


package helpers

import (
   "log"
   "os"
   "strconv"

   "github.com/joho/godotenv"
)

var AppConfig struct {
   PORT        string
   DB_HOST     string
   DB_PORT     int
   DB_USER     string
   DB_PASSWORD string
   DB_NAME     string
   SECRET_KEY  string
}

func LoadConfig(filename string) {
   err := godotenv.Load(filename)
   if err != nil {
       log.Fatal("Error loading .env file:", err)
       return
   }
   port := os.Getenv("PORT")

   if port == "" {
       port = "8000"
   }

   db_port, _ := strconv.Atoi(os.Getenv("DB_PORT"))

   AppConfig.PORT = port
   AppConfig.DB_HOST = os.Getenv("DB_HOST")
   AppConfig.DB_PORT = db_port
   AppConfig.DB_USER = os.Getenv("DB_USER")
   AppConfig.DB_PASSWORD = os.Getenv("DB_PASSWORD")
   AppConfig.DB_NAME = os.Getenv("DB_NAME")
   AppConfig.SECRET_KEY = os.Getenv("SECRET_KEY")

}

In this file, we are using the godotenv package to load the environment variables from the .env file. We then map the environmental variables to the AppConfig struct.
Coming to the connection.go file, to connect to the database, the Connect() function handles the database connection process. It creates a formatted string called dsn that contains the necessary information to connect to the PostgreSQL database. We are using the values set in our helpers package to create this string.
Then, it opens the database connection using the () function from the gorm package. The first argument to this function is the postgres package and the second argument is an empty gorm.Config{} struct.
If there are any errors during the database connection process, the panic() function will be called, which will stop the program execution.
If the connection is successful, the log package will print a message indicating that the connection was established. Then, it sets the log level of the GORM logger and auto-migrates the User model to create the necessary database tables. Finally, it sets the Database variable to the instance of the connected database.
We will then call this Connect() function inside the main.go file to establish a connection:


package main

import (
   "go-jwt-authentication/database"
   "go-jwt-authentication/helpers"

   "github.com/gin-gonic/gin"
)


func main() {
   helpers.LoadConfig(".env")
   database.Connect()

   router := gin.New()
   router.Run(":" + helpers.AppConfig.PORT)
}

Creating and Validating JWT Tokens

Generating JWT tokens

To generate JWT tokens in Go, we will be using the jwt-go package. First, we need to define a function that takes a user object and returns a signed JWT token. Here is an example implementation of such a function:


func GenerateToken(user *models.User, tokenType string) (string, error) {
   var expirationTime int64
   var subject string

   if tokenType == "access" {
       expirationTime = time.Now().Add(time.Hour * 1).Unix() // expire in 1 hour
   } else if tokenType == "refresh" {
       expirationTime = time.Now().Add(time.Hour * 24 * 7).Unix() // expire in 7 days
       subject = "refresh"
   }

   claims := Claims{
       UserID: user.ID,
       Email:  user.Email,
       StandardClaims: jwt.StandardClaims{
           ExpiresAt: expirationTime,
           Issuer:    "myapp",
           Subject:   subject,
       },
   }

   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

   accessToken, err := token.SignedString([]byte(AppConfig.SECRET_KEY))

   if err != nil {
       return "", err
   }
   return accessToken, nil
}

In this example, we define a function called GenerateToken that takes a User object and a string tokenType, and returns a signed JWT token. We set the expiration time for the access token to 1 hour from the current time and 7 days from the current time for the refresh token, and create a set of JWT claims that includes the user's id, the user's email, the expiration time, and the issuer of the token. We then create a new token using the jwt-go package, specifying the HS256 algorithm for signing the token and passing it in the JWT claims. Finally, we sign the token using our secret key and return the signed token.

The JWT claims is a struct that looks like this:


type Claims struct {
   UserID uint   `json:"user_id"`
   Email  string `json:"email"`
   jwt.StandardClaims
}

Verifying JWT tokens

To verify JWT tokens in Go, we will also be using the jwt-go package. We can define a function that takes a JWT token and our secret key, and returns the claims contained in the token if it is valid. Here is an example implementation of such a function:


func ValidateToken(tokenString string) (*Claims, error) {
   token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
       if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
           return nil, fmt.Errorf("invalid token signing method")
       }
       return []byte(AppConfig.SECRET_KEY), nil
   })
   if err != nil {
       return nil, err
   }

   claims, ok := token.Claims.(*Claims)
   if !ok || !token.Valid {
       return nil, errors.New("invalid token")
   }

   if tokenType == "refresh" && claims.Subject != "refresh" {
       return nil, errors.New("invalid token")
   }

   return claims, nil
}

In this example, we define a function called ValidateToken that takes a JWT token as a string and returns the claims contained in the token if it is valid. We first parse the token using the jwt-go package, specifying the Claims struct to decode the JWT claims. We then verify that the token is signed using the HS256 algorithm and our secret key. If the token is valid, we return the decoded claims. If the token is invalid, we return an error.

With these two functions, we can generate and verify JWT tokens in our Go web application, and use them to implement authentication and authorization in our API.

Implementing JWT middleware

In the previous section, we learned how to create and validate JWT tokens in Go. In this section, we will explore how to implement JWT middleware for Go.

What is middleware?

Middleware is a way to intercept requests and responses in the HTTP request-response cycle. It provides a mechanism to execute additional logic before or after an HTTP request is processed by a handler. Middleware can be used to perform tasks such as logging, authentication, authorization, rate limiting, and caching.

Adding middleware to Gin Gonic handlers

Gin Gonic is a popular web framework for Go that includes middleware support out of the box. Middleware functions can be added to individual routes or to all routes using the Use() method.

Here’s an example of adding a middleware function to a route in Gin:


// main.go
package main

import (
   "go-jwt-authentication/database"
   "go-jwt-authentication/helpers"

   "github.com/gin-gonic/gin"
)


func main() {
   helpers.LoadConfig(".env")
   database.Connect()

   router := gin.New()
   router.Use(gin.Logger())

   routes.AuthRoutes(router)
   router.Use(middleware.AuthMiddleware())
   routes.UserRoutes(router)

   router.Run(":" + helpers.AppConfig.PORT)
}

Here we define two sets of routes, AuthRoutes and UserRoutes. AuthRoutes handles the routes for authentication, whereas the UserRoutes contains the protected routes for user details. We will come back to defining our routes in later sections of the blog. For now, we will focus on the AuthMiddleware middleware. Here's an example of a JWT middleware function in Gin Gonic:


// authMiddleware.go
func AuthMiddleware() gin.HandlerFunc {
   return func(c *gin.Context) {
       jwt_token, err := c.Cookie("jwt")
       if err != nil {
           c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
           return
       }

       claims, err := helpers.ValidateToken(jwt_token, "access")

       if err != nil {
           c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
       }

       c.Set("userID", claims.UserID)
       c.Next()
   }
}

In this example, the AuthMiddleware() function is added as middleware to the routes UserRoutes. If the middleware logic fails (i.e. the JWT token is invalid), the middleware function aborts the request and returns an error response. If the authentication succeeds, the middleware function sets the UserID parsed from the JWT token and passes control to the next handler in the chain.

Conclusion

In conclusion, we have covered the initial setup of the project, including installing Go and the required dependencies, creating a new project, and adding the necessary packages. We also set up the database connection and migrated the user model. We then implemented JWT authentication by creating and validating tokens and adding middleware to protect routes. By the end of Part 1, we have a solid foundation to build upon for the rest of the series, where we will cover storing JWT in cookies and creating authentication routes.

Hello, I'm Rekib Ahmed. I am passionate about all things tech, with an interest in cooking, reading, and the always-changing world of innovation.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.