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/v5JWT 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
- Use HTTPS in production
- Store secrets securely (environment variables, secret management)
- Implement proper password hashing (bcrypt, scrypt)
- Use secure random tokens
- Implement token refresh mechanisms
- Validate all inputs thoroughly
- Log security events appropriately
- Implement rate limiting to prevent abuse
- Use short-lived tokens (JWT expiration)
- 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: