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.