Skip to main content

Gin Authentication Middleware

Authentication is a critical component of web applications that need to protect resources and verify user identities. In this guide, we'll explore how to implement authentication middleware in Gin, allowing you to secure routes and control access to your application.

What is Authentication Middleware?

Authentication middleware acts as a gatekeeper for your application routes. It intercepts requests before they reach the actual handler functions and verifies whether the user is authenticated. If authentication succeeds, the request proceeds; if it fails, the middleware can reject the request and return an appropriate error response.

Why Use Authentication Middleware?

  • Security: Protect sensitive routes from unauthorized access
  • Code Reusability: Write authentication logic once and apply it to multiple routes
  • Separation of Concerns: Keep authentication logic separate from your business logic
  • Flexibility: Easily apply different authentication strategies to different routes

Basic Authentication Middleware

Let's start with a simple example of basic authentication middleware in Gin:

go
package main

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

func BasicAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// Get the Basic Authentication credentials
user, password, hasAuth := c.Request.BasicAuth()

if !hasAuth {
c.Header("WWW-Authenticate", "Basic realm=Restricted")
c.AbortWithStatus(http.StatusUnauthorized)
return
}

// Here you would typically check credentials against a database
// This is a simple example with hardcoded values
if user != "admin" || password != "password" {
c.Header("WWW-Authenticate", "Basic realm=Restricted")
c.AbortWithStatus(http.StatusUnauthorized)
return
}

// Set the user in the context for later use
c.Set("user", user)
c.Next()
}
}

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

// Public route
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Welcome to the public area!",
})
})

// Protected route group
authorized := r.Group("/admin")
authorized.Use(BasicAuth())
{
authorized.GET("/dashboard", func(c *gin.Context) {
// Access the user we set in the middleware
user, _ := c.Get("user")
c.JSON(200, gin.H{
"message": "Welcome to the admin dashboard!",
"user": user,
})
})
}

r.Run(":8080")
}

How it works:

  1. We define a BasicAuth middleware function that returns a gin.HandlerFunc
  2. Inside, we extract the basic authentication credentials from the request
  3. We verify these credentials (in a real application, you'd check against a database)
  4. If authentication fails, we abort the request with a 401 Unauthorized status
  5. If authentication succeeds, we store the user in the context with c.Set() and call c.Next() to proceed to the next handler

JWT Authentication Middleware

JSON Web Tokens (JWT) provide a more sophisticated approach to authentication. Let's implement JWT authentication middleware:

go
package main

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

// Secret key for signing tokens
var secretKey = []byte("your_super_secret_key")

// GenerateToken creates a new JWT token
func GenerateToken(userID string) (string, error) {
// Create the claims
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours
}

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

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

return tokenString, nil
}

// JWTAuth middleware validates JWT tokens
func JWTAuth() 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
}

// Check for the Bearer prefix
bearerToken := strings.Split(authHeader, " ")
if len(bearerToken) != 2 || bearerToken[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
c.Abort()
return
}

tokenString := bearerToken[1]

// Parse and validate the token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return secretKey, nil
})

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

// Extract claims
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Set user ID in context
c.Set("userID", claims["user_id"])
c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
}
}

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

// Login endpoint to generate a token
r.POST("/login", func(c *gin.Context) {
var loginData struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

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

// In a real app, you'd verify against a database
if loginData.Username == "admin" && loginData.Password == "password" {
token, err := GenerateToken(loginData.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not generate token"})
return
}

c.JSON(http.StatusOK, gin.H{
"token": token,
})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
}
})

// Protected routes
protected := r.Group("/api")
protected.Use(JWTAuth())
{
protected.GET("/profile", func(c *gin.Context) {
userID, _ := c.Get("userID")
c.JSON(http.StatusOK, gin.H{
"message": "You have access to the protected endpoint!",
"userID": userID,
})
})
}

r.Run(":8080")
}

Testing JWT Authentication

To test the JWT authentication:

  1. Get a token by making a POST request to /login:
POST http://localhost:8080/login
Content-Type: application/json

{
"username": "admin",
"password": "password"
}

Response:

json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
  1. Use the token to access protected routes:
GET http://localhost:8080/api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

json
{
"message": "You have access to the protected endpoint!",
"userID": "admin"
}

Role-Based Authentication Middleware

For more complex applications, you might want to implement role-based authentication:

go
package main

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

// RoleAuth middleware checks if user has the required role
func RoleAuth(requiredRole string) gin.HandlerFunc {
return func(c *gin.Context) {
// First run the JWT authentication
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}

bearerToken := strings.Split(authHeader, " ")
if len(bearerToken) != 2 || bearerToken[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
c.Abort()
return
}

tokenString := bearerToken[1]

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return secretKey, nil
})

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

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Check if the user has the required role
userRole, exists := claims["role"].(string)
if !exists || userRole != requiredRole {
c.JSON(http.StatusForbidden, gin.H{"error": "You don't have permission to access this resource"})
c.Abort()
return
}

// User has the required role, set claims and continue
c.Set("userID", claims["user_id"])
c.Set("userRole", claims["role"])
c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
}
}

// Modified GenerateToken function that includes role
func GenerateTokenWithRole(userID string, role string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"role": role,
"exp": time.Now().Add(time.Hour * 24).Unix(),
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

tokenString, err := token.SignedString(secretKey)
if err != nil {
return "", err
}

return tokenString, nil
}

// Example usage in main function
func roleBasedExample() {
r := gin.Default()

// Login endpoint that includes role in the token
r.POST("/login", func(c *gin.Context) {
var loginData struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

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

// In a real app, you'd get the role from a database
var role string
if loginData.Username == "admin" && loginData.Password == "password" {
role = "admin"
} else if loginData.Username == "user" && loginData.Password == "password" {
role = "user"
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}

token, err := GenerateTokenWithRole(loginData.Username, role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not generate token"})
return
}

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

// Admin-only routes
adminRoutes := r.Group("/admin")
adminRoutes.Use(RoleAuth("admin"))
{
adminRoutes.GET("/dashboard", func(c *gin.Context) {
userID, _ := c.Get("userID")
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to the admin dashboard!",
"userID": userID,
})
})
}

// User-accessible routes
userRoutes := r.Group("/user")
userRoutes.Use(RoleAuth("user"))
{
userRoutes.GET("/profile", func(c *gin.Context) {
userID, _ := c.Get("userID")
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to your profile page!",
"userID": userID,
})
})
}
}

Combining Multiple Authentication Strategies

For certain applications, you might want to support multiple authentication methods:

go
package main

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

// MultiAuth middleware tries different authentication methods
func MultiAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// First check for JWT token
authHeader := c.GetHeader("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
// JWT authentication logic...
bearerToken := strings.Split(authHeader, " ")
if len(bearerToken) == 2 {
tokenString := bearerToken[1]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})

if err == nil && token.Valid {
if claims, ok := token.Claims.(jwt.MapClaims); ok {
c.Set("userID", claims["user_id"])
c.Set("authMethod", "jwt")
c.Next()
return
}
}
}
}

// If JWT fails, try basic auth
user, password, hasAuth := c.Request.BasicAuth()
if hasAuth {
// Basic auth logic...
if user == "admin" && password == "password" {
c.Set("user", user)
c.Set("authMethod", "basic")
c.Next()
return
}
}

// If API key is provided, try that
apiKey := c.GetHeader("X-API-Key")
if apiKey != "" {
// API key logic...
if apiKey == "valid-api-key-123" {
c.Set("apiUser", "api-client")
c.Set("authMethod", "api-key")
c.Next()
return
}
}

// All authentication methods failed
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
c.Abort()
}
}

Best Practices for Authentication Middleware

  1. Never Store Passwords in Plain Text: Always hash passwords using strong algorithms like bcrypt, Argon2, or scrypt.

  2. Use HTTPS: All authentication should happen over secure connections to prevent credential theft.

  3. Set Appropriate Token Expiration: JWT tokens should have a reasonable expiration time to limit the damage if they're compromised.

  4. Implement Refresh Tokens: For longer sessions, use refresh tokens instead of extending access token lifetimes.

  5. Rate Limiting: Implement rate limiting to prevent brute force attacks.

  6. Log Authentication Events: Keep logs of login attempts, failures, and suspicious activities.

  7. Use Secure Cookies: When storing tokens in cookies, use the Secure, HttpOnly, and SameSite attributes.

  8. Don't Put Sensitive Data in JWT Payloads: The payload of a JWT can be easily decoded, so avoid storing sensitive data there.

Here's an example of rate limiting middleware to prevent brute force attacks:

go
package main

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

// Simple in-memory rate limiter
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
}

func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
}
}

func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()

limiter, exists := i.ips[ip]
if !exists {
limiter = rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
}

return limiter
}

func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
l := limiter.GetLimiter(ip)
if !l.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
})
c.Abort()
return
}
c.Next()
}
}

// Example usage
func rateLimitExample() {
r := gin.Default()

// Create a rate limiter: 5 requests per minute
limiter := NewIPRateLimiter(rate.Every(time.Minute/5), 1)

// Apply rate limiting to login endpoint
r.POST("/login", RateLimitMiddleware(limiter), func(c *gin.Context) {
// Login logic here...
})
}

Summary

Authentication middleware is a critical component of web application security. In this guide, we've explored several approaches to implementing authentication in Gin:

  • Basic Authentication for simple use cases
  • JWT-based authentication for stateless, scalable auth
  • Role-based authentication for fine-grained access control
  • Multi-authentication strategies for flexible applications
  • Rate limiting to prevent brute force attacks

By implementing proper authentication middleware, you can secure your Go web applications and ensure that only authorized users can access protected resources.

Additional Resources

Exercises

  1. Implement a middleware that requires different roles for different routes.
  2. Create a middleware that logs all authentication attempts to a file.
  3. Build a authentication system that uses database-backed user credentials.
  4. Implement a session-based authentication system with Redis for storing session data.
  5. Add two-factor authentication support to the basic authentication example.


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