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)