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>"
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!