Build a JWT Authentication System in Golang with Gin

by Didin J. on May 31, 2025 Build a JWT Authentication System in Golang with Gin

Learn how to build a secure JWT authentication system in Golang with Gin and PostgreSQL, including login, refresh tokens, logout, and protected routes

Securing REST APIs is a fundamental aspect of modern web development, and one of the most efficient and scalable methods is through JWT (JSON Web Tokens). In this step-by-step tutorial, you'll learn how to implement a secure JWT authentication system using the Go programming language (Golang), the lightweight Gin web framework, and PostgreSQL for data storage. By the end of this guide, you’ll be able to build a robust authentication flow that includes user registration, login, access token generation, refresh tokens, and logout functionality—all powered by secure JWT practices. This is an ideal setup for any developer building secure RESTful APIs in Go.


Prerequisites

  • Go installed on your machine (1.20+ recommended)
go version
go version go1.24.3 darwin/arm64
  • PostgreSQL is installed and running
  • Basic understanding of Go, REST APIs, and SQL
  • Familiarity with HTTP headers and JSON format


Project Folder Structure

Here’s a suggested folder structure for organizing your project:

go-jwt-auth/
├── config/
│   └── db.go
├── controllers/
│   ├── auth.go
│   └── middleware.go
├── models/
│   └── user.go
├── utils/
│   └── jwt.go
├── .env
├── go.mod
├── go.sum
└── main.go
  • config/: Database connection setup
  • controllers/: Handlers for authentication and middleware
  • models/: GORM models
  • utils/: Utility functions for JWT
  • .env: Environment variables
  • main.go: Entry point of the application


Project Setup

Start by creating a new Go project and initializing a module:

mkdir go-jwt-auth && cd go-jwt-auth
go mod init github.com/yourusername/go-jwt-auth

Next, install the required packages:

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

Create the required folder and files for this project.

mkdir config controllers models utils
touch .env
touch main.go
touch config/db.go
touch controllers/auth.go
touch controllers/middleware.go
touch models/user.go
touch utils/jwt.go


PostgreSQL Configuration and Database Connection

Create a .env file to store database credentials:

DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=yourpassword
DB_NAME=go_jwt_auth
DB_PORT=5432

Create a new PostgreSQL database:

psql postgres -U djamware
create database go_jwt_auth;
\q

Then load the config and connect to the database. In config/db.go:

package config

import (
    "fmt"
    "log"
    "os"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "github.com/joho/godotenv"
)

var DB *gorm.DB

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

    dsn := fmt.Sprintf(
        "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
        os.Getenv("DB_HOST"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"), os.Getenv("DB_PORT"),
    )

    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect to database: ", err)
    }

    DB = db
}


User Model and Migration

Define the user model and migrate it. In models/user.go:

package models

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Username string `gorm:"unique" json:"username"`
    Password string `json:"password"`
    RefreshToken string `json:"refresh_token"`
}

Run migration in main.go:

package main

import (
    "github.com/didinj/go-jwt-auth/config"
    "github.com/didinj/go-jwt-auth/models"
)

func main() {
    config.ConnectDB()
    config.DB.AutoMigrate(&models.User{})
}


Password Hashing and JWT

Add utility functions to hash and verify passwords, and generate JWTs. In utils/jwt.go:

package utils

import (
    "time"

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

var jwtKey = []byte("my_secret_key")
var RefreshSecret = []byte("my_refresh_secret")

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    return string(bytes), err
}

func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func GenerateJWT(username string) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "username": username,
        "exp":      time.Now().Add(time.Hour * 1).Unix(),
    })
    return token.SignedString(jwtKey)
}

func GenerateRefreshToken(username string) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "username": username,
        "exp":      time.Now().Add(time.Hour * 24 * 7).Unix(),
    })
    return token.SignedString(RefreshSecret)
}


Register, Login, Refresh, and Logout Handlers

In controllers/auth.go:

package controllers

import (
    "net/http"

    "github.com/didinj/go-jwt-auth/config"
    "github.com/didinj/go-jwt-auth/models"
    "github.com/didinj/go-jwt-auth/utils"
    "github.com/golang-jwt/jwt/v5"

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

func Register(c *gin.Context) {
    var user models.User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    var existing models.User
    if err := config.DB.Where("username = ?", user.Username).First(&existing).Error; err == nil {
        c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
        return
    }

    hashedPassword, _ := utils.HashPassword(user.Password)
    user.Password = hashedPassword
    config.DB.Create(&user)
    c.JSON(http.StatusOK, gin.H{"message": "User registered successfully"})
}

func Login(c *gin.Context) {
    var input models.User
    var user models.User

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    if err := config.DB.Where("username = ?", input.Username).First(&user).Error; err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    if !utils.CheckPasswordHash(input.Password, user.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    token, _ := utils.GenerateJWT(user.Username)
    refreshToken, _ := utils.GenerateRefreshToken(user.Username)

    user.RefreshToken = refreshToken
    config.DB.Save(&user)

    c.JSON(http.StatusOK, gin.H{"token": token, "refresh_token": refreshToken})
}

func RefreshToken(c *gin.Context) {
    var req struct {
        RefreshToken string `json:"refresh_token"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    token, err := jwt.Parse(req.RefreshToken, func(token *jwt.Token) (interface{}, error) {
        return utils.RefreshSecret, nil
    })
    if err != nil || !token.Valid {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid refresh token"})
        return
    }

    claims := token.Claims.(jwt.MapClaims)
    username := claims["username"].(string)

    var user models.User
    config.DB.Where("username = ?", username).First(&user)
    if user.RefreshToken != req.RefreshToken {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Token mismatch"})
        return
    }

    newToken, _ := utils.GenerateJWT(username)
    c.JSON(http.StatusOK, gin.H{"token": newToken})
}

func Logout(c *gin.Context) {
    username, _ := c.Get("username")
    var user models.User
    config.DB.Where("username = ?", username).First(&user)
    user.RefreshToken = ""
    config.DB.Save(&user)
    c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}


JWT Middleware and Protected Route

In controllers/middleware.go:

package controllers

import (
    "net/http"
    "strings"

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

var jwtKey = []byte("my_secret_key")

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing token"})
            c.Abort()
            return
        }

        tokenStr := strings.TrimPrefix(authHeader, "Bearer ")

        token, _ := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) {
            return jwtKey, nil
        })

        if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
            c.Set("username", claims["username"])
            c.Next()
        } else {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
        }
    }
}

func Protected(c *gin.Context) {
    username, _ := c.Get("username")
    c.JSON(http.StatusOK, gin.H{"message": "Welcome " + username.(string)})
}


Gin Router Setup

In main.go:

package main

import (
    "github.com/didinj/go-jwt-auth/config"
    "github.com/didinj/go-jwt-auth/controllers"
    "github.com/didinj/go-jwt-auth/models"
    "github.com/gin-gonic/gin"
)

func main() {
    config.ConnectDB()
    config.DB.AutoMigrate(&models.User{})

    r := gin.Default()

    r.POST("/register", controllers.Register)
    r.POST("/login", controllers.Login)
    r.POST("/refresh", controllers.RefreshToken)
    r.POST("/logout", controllers.AuthMiddleware(), controllers.Logout)

    protected := r.Group("/api")
    protected.Use(controllers.AuthMiddleware())
    protected.GET("/protected", controllers.Protected)

    r.Run(":8080")
}


Test the API Using CURL

Run the app from the terminal:

go run main.go

Test the Go Rest API using CURL in another terminal tab:

# Register
curl -X POST http://localhost:8080/register \
  -H "Content-Type: application/json" \
  -d '{"username": "jane", "password": "secret"}'

# Login
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username": "jane", "password": "secret"}'

# Use Access Token for Protected Route
curl -X GET http://localhost:8080/api/protected \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

# Refresh Token
curl -X POST http://localhost:8080/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "<REFRESH_TOKEN>"}'

# Logout
curl -X POST http://localhost:8080/logout \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Build a JWT Authentication System in Golang with Gin - curl


Conclusion

Congratulations! You’ve just built a secure JWT authentication system in Go using Gin and PostgreSQL. This foundational setup includes secure password hashing with bcrypt, token-based login using JWTs, and middleware to protect your API routes. You can now extend this project with advanced features like token refresh, role-based access control, email verification, and HTTPS for production readiness.

This Golang JWT tutorial is ideal for developers looking to secure RESTful APIs and create robust backend systems. Stay tuned for future updates where we’ll expand on this setup with more real-world authentication strategies.

You can get the full working source code on our GitHub.

That is just the basics. If you need more deep learning about the Golang (Go) language and frameworks, you can take the following cheap course:

  • Collaboration and Crawling W/ Golang - Google's Go Language
  • How to Build a Golang Blog Engine Web Application
  • Fundamentals of Go Learn GoLang Programming Language
  • How to Build 1 Million Requests per Minute with Golang
  • The Complete React & Golang Course

Thanks!