Secure Your Go Web Application: JWT Authentication

SagarTS
readytowork, Inc.
Published in
5 min readSep 30, 2023

In the world of web development, security is paramount. One of the critical aspects of securing your Go web application is implementing JWT (JSON Web Token) authentication. In this article, we’ll explore how to integrate JWT authentication into your Go application using the Gin web framework, GORM for database operations, Godotenv for environment variable management, Bcrypt for password hashing, and Go-JWT for JWT handling. To enhance our development experience, we’ll also utilize Compile-daemon for automatic code reloading.

Step 1: Setting Up the Environment

Before we dive into JWT authentication, let’s ensure our development environment is properly configured. Ensure that you have Go installed on your system. If not, you can download and install it from the official website (https://golang.org/dl/).

Now, let’s create a new Go module for our project:

go mod init gin-jwt-auth // you can use any name here after init

Install the necessary dependencies:

go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/joho/godotenv
go get -u golang.org/x/crypto/bcrypt
go get -u github.com/golang-jwt/jwt/v5
go get github.com/githubnemo/CompileDaemon

Step 2: Database Setup

In this example, we’ll use PostgreSQL for simplicity, but you can adapt the code to work with your preferred database. Define your database models in a models.go file:

package models

import "gorm.io/gorm"

type User struct{
gorm.Model
Email string `gorm:"unique"`
Password string
}

The gorm:”unique” above is a database constraint that prevents multiple records from having the same value for Email. gorm.Model used is a struct that includes fields ID, CreatedAt, UpdatedAt, DeletedAt .So the type User actually looks like this:

type User struct{
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Email string `gorm:"unique"`
Password string
}

Now, let’s load env variables in loadEnvVariables.go file:

package initializers

import (
"log"

"github.com/joho/godotenv"
)

func LoadEnvVariables(){
err := godotenv.Load()

if err != nil {
log.Fatal("Error loading .env file")
}
}

We use this to load env variables like PORT, DSN & SECRET_KEY. Here PORT is on which port you would like your go project to run, DSN is Data Source Name we get from Postgres to link to it and SECRET will be our random key that we will use to generate a signed token for authentication.

After that, let’s write a code to connect to database in connectToDB.go file:

package initializers

import (
"os"

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

var DB *gorm.DB

func ConnectToDb(){
var err error
dsn := os.Getenv("DB")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

if err != nil {
panic("Failed to connect to DB")
}
}

We also create file to sync database in syncDatabase.go:

package initializers

import "gin-jwt-auth/models"

func SyncDatabase(){
DB.AutoMigrate(&models.User{})
}

Step 3: User Registration and Authentication

Create routes and functions for user registration and authentication in main.gofile. For instance:

func main(){
r := gin.Default()
r.POST("/signup", controllers.Signup)
r.POST("/login", controllers.Login)
r.GET("/validate", middleware.RequireAuth ,controllers.Validate). // here RequireAuth is a middleware that we will be creating below. It protects the route

r.Run()
}

Step 4: Add required controllers

We now add a controller function to control our routes defined in main.go file

package controllers

import (
"gin-jwt-auth/initializers"
"gin-jwt-auth/models"
"net/http"
"os"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)

func Signup(c *gin.Context){
// Get the email/pass off req Body
var body struct{
Email string
Password string
}

if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Failed to read body",
})

return
}

// Hash the password
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password),10)

if err != nil {
c.JSON(http.StatusBadRequest,gin.H{
"error": "Failed to hash password.",
})
return
}

// Create the user
user := models.User{Email: body.Email, Password: string(hash)}

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

if result.Error != nil {
c.JSON(http.StatusBadRequest,gin.H{
"error": "Failed to create user.",
})
}

// Respond
c.JSON(http.StatusOK, gin.H{})
}

func Login (c *gin.Context){
// Get email & pass off req body
var body struct{
Email string
Password string
}

if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Failed to read body",
})

return
}

// Look up for requested user
var user models.User

initializers.DB.First(&user, "email = ?", body.Email)

if user.ID == 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid email or password",
})
return
}

// Compare sent in password with saved users password
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password))

if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid email or password",
})
return
}

// Generate a JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
})

// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))

if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Failed to create token",
})
return
}

// Respond
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("Authorization", tokenString, 3600 * 24 * 30, "", "", false, true)

c.JSON(http.StatusOK, gin.H{})
}

func Validate(c *gin.Context){
user,_ := c.Get("user")

// user.(models.User).Email --> to access specific data

c.JSON(http.StatusOK, gin.H{
"message": user,
})
}

Step 5: JWT Middleware for Authentication

To secure routes, create a JWT middleware that validates tokens. Here’s a simplified example:

package middleware

import (
"fmt"
"gin-jwt-auth/initializers"
"gin-jwt-auth/models"
"net/http"
"os"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)

func RequireAuth(c *gin.Context){
// Get the cookie off the request
tokenString,err := c.Cookie("Authorization")

if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
}

// Decode/validate it
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return []byte(os.Getenv("SECRET")), nil
})

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Chec k the expiry date
if float64(time.Now().Unix()) > claims["exp"].(float64) {
c.AbortWithStatus(http.StatusUnauthorized)
}

// Find the user with token Subject
var user models.User
initializers.DB.First(&user,claims["sub"])

if user.ID == 0 {
c.AbortWithStatus(http.StatusUnauthorized)
}

// Attach the request
c.Set("user",user)

//Continue
c.Next()
} else {
c.AbortWithStatus(http.StatusUnauthorized)
}
}

Apply this middleware to protect routes that require authentication.

So, the final main.go file looks like this

package main

import (
"gin-jwt-auth/controllers"
"gin-jwt-auth/initializers"
middleware "gin-jwt-auth/milddleware"

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

func init(){
initializers.LoadEnvVariables()
initializers.ConnectToDb()
initializers.SyncDatabase()
}

func main(){
r := gin.Default()
r.POST("/signup", controllers.Signup)
r.POST("/login", controllers.Login)
r.GET("/validate", middleware.RequireAuth ,controllers.Validate)

r.Run()
}

Now, Start Compile-daemon to automatically reload your code:

compiledaemon --command="./gin-jwt-auth"   //"./GO_MOD_NAME"

To check, you can open this in postman.

Try running validate in Postman i.e localhost:4000/validate. It should throw 401 Unauthorize message. Now Register a user (/signup) and log in with that user (/login). After this, try running validate. This should work and should show result like this:

{
"message": {
"ID": 1,
"CreatedAt": "2023-09-21T00:19:06.879733+05:45",
"UpdatedAt": "2023-09-21T00:19:06.879733+05:45",
"DeletedAt": null,
"Email": "user@mailinator.com",
"Password": "$hashed_password"
}
}

Conclusion

In this guide, we’ve discovered how to enhance the security of your Go web application using JWT authentication. We’ve covered the groundwork by configuring your development environment and creating a link to your database. We’ve also tackled the crucial aspects of user registration and authentication, all while ensuring that your routes remain secure with JWT middleware. Keep in mind that you can adapt and extend these principles to match the unique requirements of your project.

You can find the codes in this repo.

Happy coding!

--

--