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:
# 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
:
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
:
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
:
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
:
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
:
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
:
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:
- Start the server:
go run main.go
- Login to get a token:
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123"}'
Example response:
{
"message": "login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "testuser",
"email": "[email protected]"
}
}
- Access protected endpoint with the token:
curl -X GET http://localhost:8080/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Example response:
{
"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
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:
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:
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:
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:
- User model and storage
- JWT token generation and validation
- Authentication middleware
- Login endpoint
- 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:
- JWT Official Website - Learn more about JWT and debug tokens
- Gin Framework Documentation - Official Gin documentation
- golang-jwt/jwt - The JWT library used in this tutorial
Exercises
- Implement a user registration endpoint that hashes passwords before storing
- Add token refresh functionality to extend user sessions
- Implement role-based access control using JWT claims
- Add token blacklisting to handle user logout and token revocation
- 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! :)