Skip to main content

Gin JWT Implementation

Introduction

JSON Web Tokens (JWT) have become a standard method for securely transmitting information between parties. In modern web applications, JWTs provide a stateless authentication mechanism that scales well and provides better user experiences. This guide will walk you through implementing JWT authentication in a Gin web application.

JWT consists of three parts:

  • Header: Contains metadata about the token (type and algorithm)
  • Payload: Contains claims (user information and additional data)
  • Signature: Ensures the token hasn't been altered

By the end of this tutorial, you'll understand how to:

  • Generate and validate JWTs in Gin
  • Implement login and registration endpoints
  • Protect routes using JWT middleware
  • Handle token refreshing and expiration

Setting Up Your Project

Let's start by setting up a new Gin project with the necessary dependencies:

bash
# Create a new project directory
mkdir gin-jwt-auth
cd gin-jwt-auth

# Initialize Go module
go mod init github.com/yourusername/gin-jwt-auth

# Install required dependencies
go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v4

Basic Project Structure

Let's organize our code using the following structure:

gin-jwt-auth/
├── main.go
├── auth/
│ ├── jwt.go
│ └── middleware.go
├── controllers/
│ └── user_controller.go
├── models/
│ └── user.go
└── routes/
└── routes.go

Creating the User Model

First, let's define our user model. Create a file named models/user.go:

go
package models

import "time"

// User represents the user model
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique"`
Email string `json:"email" gorm:"unique"`
Password string `json:"-"` // Password is not exposed in JSON responses
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// For simplicity, we'll use an in-memory user store
var UserStore = []User{
{
ID: 1,
Username: "testuser",
Email: "[email protected]",
Password: "password123", // In production, this should be hashed
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}

// Find a user by username
func FindUserByUsername(username string) *User {
for _, u := range UserStore {
if u.Username == username {
return &u
}
}
return nil
}

JWT Implementation

Now, let's implement the JWT functionality in auth/jwt.go:

go
package auth

import (
"errors"
"fmt"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/yourusername/gin-jwt-auth/models"
)

// JWT secret key
var jwtKey = []byte("your_secret_key") // In production, use a secure environment variable

// Claims represents the JWT claims
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}

// GenerateToken creates a new JWT token for a user
func GenerateToken(user *models.User) (string, error) {
// Token expiration time (15 minutes)
expirationTime := time.Now().Add(15 * time.Minute)

// Create claims with user information
claims := &Claims{
UserID: user.ID,
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "gin-jwt-auth",
Subject: fmt.Sprintf("%d", user.ID),
},
}

// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

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

return tokenString, nil
}

// ValidateToken validates the JWT token
func ValidateToken(tokenString string) (*Claims, error) {
claims := &Claims{}

// Parse the token
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtKey, nil
})

if err != nil {
return nil, err
}

if !token.Valid {
return nil, errors.New("invalid token")
}

return claims, nil
}

JWT Middleware

Now, let's create a middleware to protect routes that require authentication. Create auth/middleware.go:

go
package auth

import (
"net/http"
"strings"

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

// AuthMiddleware protects routes that require authentication
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Get the Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header is required"})
c.Abort()
return
}

// Extract the token from the Bearer header
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header format must be 'Bearer {token}'"})
c.Abort()
return
}

tokenString := parts[1]

// Validate the token
claims, err := ValidateToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
c.Abort()
return
}

// Store user information in the context
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)

c.Next()
}
}

User Controller

Let's create a controller to handle user authentication in controllers/user_controller.go:

go
package controllers

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/yourusername/gin-jwt-auth/auth"
"github.com/yourusername/gin-jwt-auth/models"
)

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

// Login authenticates a user and returns a JWT token
func Login(c *gin.Context) {
var request LoginRequest

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

// Find the user by username
user := models.FindUserByUsername(request.Username)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
return
}

// In a real app, you would compare hashed passwords
if user.Password != request.Password {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
return
}

// Generate JWT token
token, err := auth.GenerateToken(user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}

c.JSON(http.StatusOK, gin.H{
"message": "login successful",
"token": token,
"user": gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
},
})
}

// Profile returns the authenticated user's profile
func Profile(c *gin.Context) {
// Get user information from the context (set by the AuthMiddleware)
userID, _ := c.Get("user_id")
username, _ := c.Get("username")

c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"username": username,
"message": "protected profile information",
})
}

Routes Configuration

Now, let's set up our routes in routes/routes.go:

go
package routes

import (
"github.com/gin-gonic/gin"
"github.com/yourusername/gin-jwt-auth/auth"
"github.com/yourusername/gin-jwt-auth/controllers"
)

// SetupRoutes configures the API routes
func SetupRoutes(router *gin.Engine) {
// Public routes
public := router.Group("/api")
{
public.POST("/login", controllers.Login)
}

// Protected routes
protected := router.Group("/api")
protected.Use(auth.AuthMiddleware())
{
protected.GET("/profile", controllers.Profile)
}
}

Main Application

Finally, let's create our main application in main.go:

go
package main

import (
"github.com/gin-gonic/gin"
"github.com/yourusername/gin-jwt-auth/routes"
)

func main() {
// Create a Gin router
router := gin.Default()

// Set up routes
routes.SetupRoutes(router)

// Start the server
router.Run(":8080")
}

Testing the JWT Implementation

Now that we've set up our JWT authentication system, let's test it:

  1. Start the server:
bash
go run main.go
  1. Login to get a token:
bash
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123"}'

Example response:

json
{
"message": "login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "testuser",
"email": "[email protected]"
}
}
  1. Access protected endpoint with the token:
bash
curl -X GET http://localhost:8080/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Example response:

json
{
"message": "protected profile information",
"user_id": 1,
"username": "testuser"
}

Real-World Enhancements

In a production application, you would want to implement several additional features:

1. Token Refresh Mechanism

go
func RefreshToken(c *gin.Context) {
// Get the user from the context (set by AuthMiddleware)
userID, _ := c.Get("user_id")
username, _ := c.Get("username")

// Find the user in the database
user := &models.User{
ID: userID.(uint),
Username: username.(string),
}

// Generate a new token
newToken, err := auth.GenerateToken(user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to refresh token"})
return
}

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

2. Password Hashing

In a real application, you should never store plain-text passwords. Use a library like golang.org/x/crypto/bcrypt to hash passwords:

go
import "golang.org/x/crypto/bcrypt"

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

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

3. Database Integration

Instead of using an in-memory store, connect to a database like PostgreSQL or MySQL using a library like GORM:

go
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

func SetupDatabase() *gorm.DB {
dsn := "host=localhost user=postgres password=postgres dbname=gin_jwt port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect to database")
}

// Auto-migrate the schema
db.AutoMigrate(&models.User{})

return db
}

4. Environment Variables

Store sensitive information like JWT keys in environment variables:

go
import "os"

var jwtKey = []byte(os.Getenv("JWT_SECRET_KEY"))

Summary

In this tutorial, we've created a complete JWT authentication system for a Gin application, including:

  1. User model and storage
  2. JWT token generation and validation
  3. Authentication middleware
  4. Login endpoint
  5. Protected routes

This implementation provides a solid foundation for authentication in your Gin applications. JWT authentication offers several advantages:

  • Stateless: No need to store session information on the server
  • Scalable: Works well in distributed systems
  • Cross-domain: Can be used across different domains and services
  • Mobile-friendly: Works well for mobile applications

Additional Resources

To further enhance your understanding of JWT authentication in Gin:

Exercises

  1. Implement a user registration endpoint that hashes passwords before storing
  2. Add token refresh functionality to extend user sessions
  3. Implement role-based access control using JWT claims
  4. Add token blacklisting to handle user logout and token revocation
  5. Implement rate limiting for login attempts to prevent brute force attacks

With these concepts and implementations, you now have a comprehensive understanding of JWT authentication in Gin applications!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)