Creating a GIN Project with Basic JWT Authentication: A Step-by-Step Guide

Securing Your Golang GIN Project with JWT Authentication: A Step-by-Step Guide

Welcome to a hassle-free guide on boosting the security of your Golang GIN framework web project. In simple steps, we’ll show you how to integrate JWT authentication, a powerful tool to keep your web app safe. Golang’s simplicity and GIN’s lightweight design make this process straightforward. Join us to make your project secure and resilient without the headaches. Let’s dive in!

Step 1: Create Directory Structure

1.1. Create a new directory for your project:

mkdir mywebapp
cd mywebapp

1.2. Initialize a new Go module:

go mod init mywebapp

1.3. Create the following directory structure:

mywebapp/
├── main.go
├── static/
│   ├── css/
│   └── js/
└── controllers/
│   └── AuthController.go
└── config/
│   └── mysql.go
└── middlewares/
│   └── JwtAuthMiddleware.go
└── models/
│   └── User.go
└── views/
└── utils/
    └── token.go
mkdir static controllers config middlewares models views utils

Step 2: Install Dependencies

Install the required dependencies:

go get github.com/gin-gonic/gin
go get golang.org/x/crypto/bcrypt
go get github.com/dgrijalva/jwt-go
go get github.com/jinzhu/gorm
go get -u github.com/jinzhu/gorm/dialects/mysql
go get github.com/joho/godotenv

Step 2: Set Up Project

2.1. Create main.go:

touch main.go
// main.go
package main

import (
	"github.com/gin-gonic/gin"
	"mywebapp/controllers"
	"mywebapp/middlewares"
	"mywebapp/models"
)

2.2. Initialize the Gin router and connect to the database:

// main.go
func main() {
	// Initialize the Gin router
	r := gin.Default()

	// Load database connection
	models.ConnectDataBase()
}

Step 3: Set Up User Model

3.1. Create models/User.go:

touch models/User.go
// models/User.go
package models

import (
	"errors"
	"html"
	"strings"

	"github.com/jinzhu/gorm"
	"golang.org/x/crypto/bcrypt"
	"mywebapp/utils"
)

type User struct {
	gorm.Model
	Username string `gorm:"size:255;not null;unique" json:"username"`
	Password string `gorm:"size:255;not null;" json:"password"`
	Name     string `gorm:"size:255;not null;" json:"name"`
	Email    string `gorm:"size:255;not null;" json:"email"`
}

3.2. Add helper functions to retrieve and verify user information:

// GetUserByID retrieves a user by ID from the database
func GetUserByID(uid uint) (User, error) {
	var u User

	if err := DB.First(&u, uid).Error; err != nil {
		return u, errors.New("User not found!")
	}

	u.PrepareGive()

	return u, nil
}

// PrepareGive removes sensitive information before sending user details
func (u *User) PrepareGive() {
	u.Password = ""
}

// GetUserByUsername retrieves a user by username from the database
func GetUserByUsername(username string) (User, error) {
	var user User

	if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return User{}, errors.New("User not found")
		}
		return User{}, err
	}

	user.PrepareGive()

	return user, nil
}

3.3. Add functions for password verification and login:

// VerifyPassword compares the provided password with the hashed password
func VerifyPassword(password, hashedPassword string) error {
	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

// LoginCheck validates user credentials and generates a token
func LoginCheck(username, password string) (string, error) {
	var err error

	u := User{}

	err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error

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

	err = VerifyPassword(password, u.Password)

	if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
		return "", err
	}

	token, err := utils.GenerateToken(u.ID)

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

	return token, nil
}

3.4. Implement functions for user creation and update:

// SaveUser creates a new user in the database
func (u *User) SaveUser() (*User, error) {
	var err error
	err = DB.Create(&u).Error
	if err != nil {
		return &User{}, err
	}
	return u, nil
}

// BeforeSave is a callback function called before saving a user
func (u *User) BeforeSave() error {
	// Turn password into hash
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	u.Password = string(hashedPassword)

	// Remove spaces in username
	u.Username = html.EscapeString(strings.TrimSpace(u.Username))

	return nil
}

// UpdateUser updates an existing user in the database
func (u *User) UpdateUser() (*User, error) {
	if u.ID == 0 {
		return u, errors.New("User not found!")
	}

	err := DB.Model(u).Updates(u).Error
	if err != nil {
		return nil, err
	}
	return u, nil
}

Step 4: Set Up Database Configuration

***temporary setup in models/mysql.go, change $models.User to User

4.1. Create config/mysql.go:

touch config/mysql.go
// config/mysql.go
package config

import (
	"fmt"
	"log"
	"os"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"github.com/joho/godotenv"
	"mywebapp/models"
)

var DB *gorm.DB

// ConnectDataBase connects to the database using environment variables
func ConnectDataBase() {
	// Load environment variables from .env file
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatalf("Error loading .env file")
	}

	// Retrieve database connection details from environment variables
	Dbdriver := os.Getenv("DB_DRIVER")
	DbHost := os.Getenv("DB_HOST")
	DbUser := os.Getenv("DB_USER")
	DbPassword := os.Getenv("DB_PASSWORD")
	DbName := os.Getenv("DB_NAME")
	DbPort := os.Getenv("DB_PORT")

	// Construct the database URL
	DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", DbUser, DbPassword, DbHost, DbPort, DbName)

	// Connect to the database
	DB, err = gorm.Open(Dbdriver, DBURL)
	if err != nil {
		fmt.Println("Cannot connect to database ", Dbdriver)
		log.Fatal("connection error:", err)
	} else {
		fmt.Println("We are connected to the database ", Dbdriver)
	}

	// AutoMigrate creates or updates database tables based on model definitions
	DB.AutoMigrate(&models.User{})
}

4.2. Create a .env file with your database configurations:

touch .env
DB_DRIVER=mysql
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=mywebapp
DB_PORT=3306

Step 5: Set Up Token Utility

5.1. Create utils/token.go:

touch utils/token.go
// utils/token.go
package utils

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	jwt "github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
)

var secretKey = os.Getenv("SECRET_KEY")

// GenerateToken generates a new JWT token for the given user ID
func GenerateToken(userID uint) (string, error) {

	token_lifespan,err := strconv.Atoi(os.Getenv("TOKEN_LIFESPAN"))

	if err != nil {
	  return "",err
	}
	// Create the token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": userID,
		"exp":     time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
	})

	// Sign the token with the secret key
	tokenString, err := token.SignedString([]byte(secretKey))
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

// ExtractTokenID extracts the user ID from the JWT token
func ExtractTokenID(tokenString string) (uint, error) {
	// Parse the token
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Check the signing method
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}

		// Return the secret key
		return []byte(secretKey), nil
	})
	if err != nil {
		return 0, err
	}

	// Check if the token is valid
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		userID := uint(claims["user_id"].(float64))
		return userID, nil
	}

	return 0, fmt.Errorf("Invalid token")
}

// TokenCookie sets the JWT token in a cookie
func TokenCookie(c *gin.Context) {
	// Get the token from the request header
	authHeader := c.GetHeader("Authorization")
	tokenString := strings.Replace(authHeader, "Bearer ", "", 1)

	// Set the token in the cookie
	c.SetCookie("token", tokenString, 0, "/", "localhost", false, true)
}

5.2. Add following to .env

SECRET_KEY=yoursecretstring
TOKEN_LIFESPAN=1

Step 6: Set Up JWT Authentication Middleware

6.1. Create middlewares/JwtAuthMiddleware.go:

touch middlewares/JwtAuthMiddleware.go
// middlewares/JwtAuthMiddleware.go
package middlewares

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

var secretKey = os.Getenv("SECRET_KEY")

// JwtAuthMiddleware is a middleware for JWT authentication
func JwtAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Get the token from the request header
		authHeader := c.GetHeader("Authorization")
		tokenString := strings.Replace(authHeader, "Bearer ", "", 1)

		// Verify the token
		token, err := utils.ExtractTokenID(tokenString)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
			c.Abort()
			return
		}

		// Set the user ID in the context for further use in the handler
		c.Set("userID", token)

		c.Next()
	}
}

Step 7: Create AuthController

7.1. Create controllers/AuthController.go:

touch controllers/AuthController.go
// controllers/AuthController.go
package controllers

import (
	"github.com/gin-gonic/gin"
	"mywebapp/models"
	"mywebapp/utils"
	"net/http"
)

7.2. Add input struct for user registration:

type RegisterInput struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
	Name     string `json:"name" binding:"required"`
	Email    string `json:"email" binding:"required,email"`
}

7.3. Implement user registration:

// Register handles user registration
func Register(c *gin.Context) {
	var input RegisterInput

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

	// Create a new user
	user := models.User{
		Username: input.Username,
		Password: input.Password,
		Name:     input.Name,
		Email:    input.Email,
	}

	// Save the user to the database
	_, err := user.SaveUser()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "User registered successfully"})
}

7.4. Add input struct for user login:

type LoginInput struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

7.5. Implement user login:

// Login handles user login
func Login(c *gin.Context) {
	var input LoginInput

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

	// Validate user credentials and generate a token
	token, err := models.LoginCheck(input.Username, input.Password)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
		return
	}

	// Set the token in a cookie
	utils.TokenCookie(c)

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

7.6. Add user profile API:

// Profile handles user profile
func Profile(c *gin.Context) {
	userID := c.MustGet("userID").(uint)

	// Retrieve user from the database
	user, err := models.GetUserByID(userID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
		return
	}

	// Return user details
	c.JSON(http.StatusOK, gin.H{
		"username": user.Username,
		"name":     user.Name,
		"email":    user.Email,
	})
}

Step 8: Implement Routes in main.go

8.1. Set up routes:

// main.go (continued)
func main() {
	// Initialize the Gin router
	r := gin.Default()

	// Load database connection
	models.ConnectDataBase()

	// Group routes
	api := r.Group("/api")
	{
		auth := api.Group("/auth")
		{
			// Register and login routes
			auth.POST("/register", controllers.Register)
			auth.POST("/login", controllers.Login)
		}

		// Profile route with JWT authentication middleware
		api.GET("/profile", middlewares.JwtAuthMiddleware(), controllers.Profile)
	}

	// Run the server
	r.Run(":8080")
}

Step 9: Implement Token Utility Functions in token.go

9.1. Move functions from JwtAuthMiddleware.go to token.go:

// TokenCookie sets the JWT token in a cookie
func TokenCookie(c *gin.Context) {
	// Get the token from the request header
	authHeader := c.GetHeader("Authorization")
	tokenString := strings.Replace(authHeader, "Bearer ", "", 1)

	// Set the token in the cookie
	c.SetCookie("token", tokenString, 0, "/", "localhost", false, true)
}

// ExtractTokenID extracts the user ID from the JWT token
func ExtractTokenID(tokenString string) (uint, error) {
	// Parse the token
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Check the signing method
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}

		// Return the secret key
		return []byte(secretKey), nil
	})
	if err != nil {
		return 0, err
	}

	// Check if the token is valid
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		userID := uint(claims["user_id"].(float64))
		return userID, nil
	}

	return 0, fmt.Errorf("Invalid token")
}

Step 10: Run and Test

10.1. Run your application:

go run main.go

10.2. Register a user:

curl -X POST -H "Content-Type: application/json" -d '{"username":"yourusername", "password":"yourpassword", "name":"Your Name", "email":"[email protected]"}' <http://localhost:8080/api/auth/register>

10.3. Login to get a token:

curl -X POST -H "Content-Type: application/json" -d '{"username":"yourusername", "password":"yourpassword"}' <http://localhost:8080/api/auth/login>

You will receive a token in the response.

10.4. Access the profile using the received token:

export TOKEN="your_received_token"

10.5. Access the profile using the obtained token:

curl -H "Authorization: Bearer $TOKEN" <http://localhost:8080/api/profile>

This should return the user’s profile details.

Now, you have a simple JWT-authenticated API with Golang GIN. Feel free to explore further and extend your application based on these foundational steps. With Golang GIN JWT, you’ve laid a robust groundwork for secure and scalable web applications. Happy coding!

Thank you for exploring FastDT. Explore our range of services to enhance your business.

Learn more about our services.