Authentication Middleware in Gin

Authentication is crucial for securing web applications. In Gin, middleware provides a clean way to implement authentication across your API endpoints. This tutorial covers different authentication methods including JWT tokens, basic auth, and API keys, with complete working examples.

Understanding Authentication Middleware

Authentication middleware in Gin sits between the incoming request and your route handlers. It validates user credentials and either allows the request to proceed or returns an authentication error.

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Authentication logic here
        token := c.GetHeader("Authorization")
        
        if token == "" {
            c.JSON(401, gin.H{"error": "Authorization header required"})
            c.Abort()
            return
        }
        
        // Validate token
        if !isValidToken(token) {
            c.JSON(401, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }
        
        c.Next() // Proceed to next handler
    }
}

JWT Authentication

JSON Web Tokens (JWT) are widely used for stateless authentication.

Installing Dependencies

go get github.com/golang-jwt/jwt/v5

JWT Middleware

package main

import (
    "net/http"
    "strings"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

var jwtKey = []byte("your-secret-key")

func JWTMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
            c.Abort()
            return
        }

        // Bearer token format: "Bearer <token>"
        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        if tokenString == authHeader {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token required"})
            c.Abort()
            return
        }

        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            return jwtKey, nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        // Store user info in context for handlers
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

func GenerateToken(userID uint, username string) (string, error) {
    expirationTime := time.Now().Add(24 * time.Hour)
    claims := &Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expirationTime),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

Using JWT Middleware

func main() {
    r := gin.Default()

    // Public routes
    r.POST("/login", loginHandler)
    r.POST("/register", registerHandler)

    // Protected routes
    protected := r.Group("/api")
    protected.Use(JWTMiddleware())
    {
        protected.GET("/profile", getProfile)
        protected.POST("/posts", createPost)
        protected.DELETE("/posts/:id", deletePost)
    }

    r.Run(":8080")
}

func loginHandler(c *gin.Context) {
    var loginReq struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    
    if err := c.ShouldBindJSON(&loginReq); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // Validate credentials (implement your own logic)
    if loginReq.Username == "admin" && loginReq.Password == "password" {
        token, err := GenerateToken(1, "admin")
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
            return
        }
        
        c.JSON(http.StatusOK, gin.H{"token": token})
    } else {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
    }
}

func getProfile(c *gin.Context) {
    userID, _ := c.Get("user_id")
    username, _ := c.Get("username")
    
    c.JSON(http.StatusOK, gin.H{
        "user_id": userID,
        "username": username,
    })
}

Basic Authentication

HTTP Basic Authentication uses username/password encoded in base64.

package main

import (
    "crypto/subtle"
    "encoding/base64"
    "net/http"
    "strings"
    
    "github.com/gin-gonic/gin"
)

func BasicAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        auth := c.GetHeader("Authorization")
        if auth == "" {
            c.Header("WWW-Authenticate", `Basic realm="Restricted"`)
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
            c.Abort()
            return
        }

        if !strings.HasPrefix(auth, "Basic ") {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header"})
            c.Abort()
            return
        }

        decoded, err := base64.StdEncoding.DecodeString(auth[6:])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid base64 encoding"})
            c.Abort()
            return
        }

        creds := strings.SplitN(string(decoded), ":", 2)
        if len(creds) != 2 {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"})
            c.Abort()
            return
        }

        username, password := creds[0], creds[1]

        // Validate credentials (use secure comparison)
        if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 &&
           subtle.ConstantTimeCompare([]byte(password), []byte("password")) == 1 {
            c.Set("username", username)
            c.Next()
        } else {
            c.Header("WWW-Authenticate", `Basic realm="Restricted"`)
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
            c.Abort()
        }
    }
}

API Key Authentication

Simple token-based authentication for API access.

package main

import (
    "net/http"
    "os"
    
    "github.com/gin-gonic/gin"
)

func APIKeyMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        apiKey := c.GetHeader("X-API-Key")
        if apiKey == "" {
            apiKey = c.Query("api_key") // Allow API key in query parameter
        }
        
        if apiKey == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "API key required"})
            c.Abort()
            return
        }

        expectedKey := os.Getenv("API_KEY")
        if expectedKey == "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Server configuration error"})
            c.Abort()
            return
        }

        if apiKey != expectedKey {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
            c.Abort()
            return
        }

        c.Next()
    }
}

Role-Based Access Control (RBAC)

Extending JWT middleware with role checking.

package main

import (
    "net/http"
    "strings"
    
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

type UserClaims struct {
    UserID   uint     `json:"user_id"`
    Username string   `json:"username"`
    Roles    []string `json:"roles"`
    jwt.RegisteredClaims
}

func RoleMiddleware(requiredRoles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // First run JWT middleware (assuming it's already applied)
        claimsInterface, exists := c.Get("claims")
        if !exists {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "No claims found"})
            c.Abort()
            return
        }

        claims, ok := claimsInterface.(*UserClaims)
        if !ok {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid claims type"})
            c.Abort()
            return
        }

        // Check if user has required role
        hasRole := false
        for _, userRole := range claims.Roles {
            for _, requiredRole := range requiredRoles {
                if userRole == requiredRole {
                    hasRole = true
                    break
                }
            }
            if hasRole {
                break
            }
        }

        if !hasRole {
            c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
            c.Abort()
            return
        }

        c.Next()
    }
}

Combining Multiple Authentication Methods

func main() {
    r := gin.Default()

    // Public routes
    r.POST("/login", loginHandler)

    // API routes with API key
    api := r.Group("/api/v1")
    api.Use(APIKeyMiddleware())
    {
        api.GET("/public-data", getPublicData)
    }

    // User routes with JWT
    user := r.Group("/api/v1/user")
    user.Use(JWTMiddleware())
    {
        user.GET("/profile", getProfile)
        user.POST("/posts", createPost)
    }

    // Admin routes with JWT + role check
    admin := r.Group("/api/v1/admin")
    admin.Use(JWTMiddleware())
    admin.Use(RoleMiddleware("admin"))
    {
        admin.GET("/users", getAllUsers)
        admin.DELETE("/users/:id", deleteUser)
    }

    r.Run(":8080")
}

Rate Limiting with Authentication

Combining authentication with rate limiting.

package main

import (
    "net/http"
    "sync"
    "time"
    
    "github.com/gin-gonic/gin"
    "golang.org/x/time/rate"
)

var (
    limiters = make(map[string]*rate.Limiter)
    mu       sync.RWMutex
)

func getUserLimiter(userID string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    
    limiter, exists := limiters[userID]
    if !exists {
        limiter = rate.NewLimiter(rate.Every(time.Minute), 10) // 10 requests per minute
        limiters[userID] = limiter
    }
    
    return limiter
}

func RateLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Get user ID from context (set by auth middleware)
        userIDInterface, exists := c.Get("user_id")
        if !exists {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
            c.Abort()
            return
        }
        
        userID, ok := userIDInterface.(uint)
        if !ok {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID"})
            c.Abort()
            return
        }
        
        limiter := getUserLimiter(fmt.Sprintf("%d", userID))
        if !limiter.Allow() {
            c.JSON(http.StatusTooManyRequests, gin.H{"error": "Rate limit exceeded"})
            c.Abort()
            return
        }
        
        c.Next()
    }
}

CORS with Authentication

Handling CORS when using authentication.

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "http://localhost:3000")
        c.Header("Access-Control-Allow-Credentials", "true")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

Complete Example

package main

import (
    "log"
    "net/http"
    "os"
    "strings"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
    "github.com/joho/godotenv"
)

type User struct {
    ID       uint   `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
    Roles    []string `json:"roles"`
}

type Claims struct {
    UserID   uint     `json:"user_id"`
    Username string   `json:"username"`
    Roles    []string `json:"roles"`
    jwt.RegisteredClaims
}

var jwtKey = []byte("your-secret-key-change-this-in-production")

func main() {
    if err := godotenv.Load(); err != nil {
        log.Println("No .env file found")
    }

    r := gin.Default()
    r.Use(CORSMiddleware())

    // Public routes
    r.POST("/login", loginHandler)
    r.POST("/register", registerHandler)

    // API routes
    api := r.Group("/api")
    api.Use(APIKeyMiddleware())
    {
        api.GET("/public", getPublicData)
    }

    // Protected routes
    protected := r.Group("/api/protected")
    protected.Use(JWTMiddleware())
    protected.Use(RateLimitMiddleware())
    {
        protected.GET("/profile", getProfile)
        protected.POST("/posts", createPost)
    }

    // Admin routes
    admin := r.Group("/api/admin")
    admin.Use(JWTMiddleware())
    admin.Use(RoleMiddleware("admin"))
    {
        admin.GET("/users", getUsers)
        admin.POST("/users", createUser)
    }

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    log.Printf("Server starting on port %s", port)
    r.Run(":" + port)
}

// Handler implementations would go here...
func loginHandler(c *gin.Context) { /* ... */ }
func registerHandler(c *gin.Context) { /* ... */ }
func getProfile(c *gin.Context) { /* ... */ }
// etc.

Security Best Practices

  1. Use HTTPS in production
  2. Store secrets securely (environment variables, secret management)
  3. Implement proper password hashing (bcrypt, scrypt)
  4. Use secure random tokens
  5. Implement token refresh mechanisms
  6. Validate all inputs thoroughly
  7. Log security events appropriately
  8. Implement rate limiting to prevent abuse
  9. Use short-lived tokens (JWT expiration)
  10. Validate token signatures properly

Summary

Authentication middleware in Gin provides flexible ways to secure your API:

  • JWT tokens for stateless authentication
  • Basic auth for simple use cases
  • API keys for service-to-service communication
  • Role-based access for fine-grained permissions
  • Rate limiting to prevent abuse
  • CORS handling for web applications

Choose the authentication method that best fits your application’s needs and always follow security best practices.


External Resources:

Related Tutorials:

  • Learn about Gin routing here to understand how routes work with middleware.
  • Check out global middleware here for other middleware patterns.
Last updated on