Building a Lightweight JWT Authentication Middleware for Go Gin Applications
Authentication is the backbone of secure web applications. Today, we'll create a clean, production-ready JWT authentication middleware for Go applications using the Gin framework.
๐ฏ What We're Building
Our middleware will handle:
- ๐ JWT token validation
- ๐ Bearer token support
- ๐ค User context extraction
- โ๏ธ Environment configuration
- ๐งช Comprehensive testing
๐๏ธ Project Setup
Let's start by setting up our project structure:
mkdir authmiddleware && cd authmiddleware
go mod init authmiddleware
Install dependencies:
go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
๐ Project Structure
authmiddleware/
โโโ config/
โ โโโ config.go
โโโ middlewares/
โ โโโ authmiddleware.go
โโโ tests/
โ โโโ auth_middleware_test.go
โ โโโ config_test.go
โโโ docs/
โโโ .env.example
โโโ go.mod
โโโ README.md
โ๏ธ Configuration Management
First, let's create our configuration handler in config/config.go:
package config
import (
"log"
"os"
"github.com/joho/godotenv"
)
type Config struct {
Port string
JWTSecret string
}
var AppConfig *Config
func LoadEnv() {
err := godotenv.Load()
if err != nil {
log.Println("No .env file found, using system environment")
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
AppConfig = &Config{
Port: port,
JWTSecret: os.Getenv("JWT_SECRET"),
}
if AppConfig.JWTSecret == "" {
log.Fatal("JWT_SECRET environment variable is required")
}
}
๐ The Authentication Middleware
Now for the main event - our JWT middleware in middlewares/authmiddleware.go:
package middlewares
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func AuthMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
// Extract token from Authorization header
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" {
ctx.JSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header is required"
})
ctx.Abort()
return
}
// Handle both "Bearer token" and direct token formats
tokenString := strings.TrimSpace(authHeader)
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
tokenString = strings.TrimSpace(tokenString[7:])
}
// Parse and validate JWT token
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
ctx.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid or expired token"
})
ctx.Abort()
return
}
// Extract claims and set context
if claims, ok := token.Claims.(jwt.MapClaims); ok {
ctx.Set("user_id", claims["user_id"])
ctx.Set("email", claims["email"])
}
ctx.Next()
}
}
๐ Usage Example
Here's how to integrate the middleware into your application:
package main
import (
"authmiddleware/config"
"authmiddleware/middlewares"
"github.com/gin-gonic/gin"
)
func main() {
config.LoadEnv()
r := gin.Default()
// Public routes
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "healthy"})
})
// Protected routes
protected := r.Group("/api/v1")
protected.Use(middlewares.AuthMiddleware())
{
protected.GET("/profile", getProfile)
protected.POST("/data", postData)
}
r.Run(":" + config.AppConfig.Port)
}
func getProfile(c *gin.Context) {
userID := c.GetString("user_id")
email := c.GetString("email")
c.JSON(200, gin.H{
"user_id": userID,
"email": email,
"message": "Profile retrieved successfully"
})
}
๐งช Testing Our Middleware
Testing is crucial! Here's our test suite in tests/auth_middleware_test.go:
package tests
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"authmiddleware/middlewares"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
os.Setenv("JWT_SECRET", "test-secret-key")
os.Exit(m.Run())
}
func createTestToken(userID, email string) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID,
"email": email,
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte("test-secret-key"))
return tokenString
}
func TestAuthMiddleware_ValidToken(t *testing.T) {
router := gin.New()
router.Use(middlewares.AuthMiddleware())
router.GET("/test", func(c *gin.Context) {
userID := c.GetString("user_id")
email := c.GetString("email")
c.JSON(200, gin.H{"user_id": userID, "email": email})
})
token := createTestToken("123", "test@example.com")
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}
func TestAuthMiddleware_MissingToken(t *testing.T) {
router := gin.New()
router.Use(middlewares.AuthMiddleware())
router.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "success"})
})
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status 401, got %d", w.Code)
}
}
๐ Security Best Practices
When implementing JWT authentication:
- Strong Secrets: Use cryptographically secure JWT secrets
- Token Expiration: Always set expiration times
- Signing Method Validation: Verify expected algorithms
- HTTPS Only: Never transmit tokens over HTTP
- Error Handling: Don't leak sensitive information
๐โโ๏ธ Running Tests
Test your implementation:
# Run all tests
go test ./tests/...
# With coverage
go test -cover ./tests/...
# Verbose output
go test -v ./tests/...
๐ฆ Environment Setup
Create .env.example:
PORT=8080
JWT_SECRET=your-super-secret-jwt-key-here
๐ What We've Accomplished
Our middleware now provides:
โ
JWT token validation
โ
Flexible token format support
โ
User context extraction
โ
Comprehensive error handling
โ
Production-ready code
โ
Full test coverage
๐ Next Steps
Consider extending with:
- Role-based authorization
- Token refresh mechanism
- Rate limiting
- Audit logging
- Multi-tenant support
๐ก Key Takeaways
Building your own auth middleware gives you:
- Complete control over authentication flow
- Better understanding of JWT security
- Lightweight, dependency-minimal solution
- Easy customization for business needs
๐ Source Code
๐ Complete source code: GitHub Repository
โญ Star the repo if this helped you! โญ
What authentication challenges have you faced in your Go applications? Share your experiences in the comments below!
Connect with me:
- ๐ GitHub: @keyadaniel56 - Follow for more Go tutorials!
- ๐ฌ Let's discuss Go best practices and build amazing things together!
Found this helpful? Give it a โค๏ธ and share with fellow developers!
Top comments (0)