How to Implement JWT Authentication in Gin/Echo/Chi

Web
Use a middleware function to intercept requests, extract the JWT from the `Authorization` header, verify the signature and expiration, and inject the user claims into the request context for downstream handlers.

Use a middleware function to intercept requests, extract the JWT from the Authorization header, verify the signature and expiration, and inject the user claims into the request context for downstream handlers. All three frameworks (Gin, Echo, Chi) support this pattern, though Gin and Echo have mature third-party middleware packages that simplify the boilerplate.

Here is a practical implementation using Gin with the popular github.com/golang-jwt/jwt/v5 library. This example demonstrates creating a token, verifying it in middleware, and accessing the payload in a handler.

1. Define the Claims structure and Middleware

package main

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
)

var jwtKey = []byte("your-secret-key") // Use env var in production

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

func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
			return
		}

		tokenString := authHeader
		if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
			tokenString = authHeader[7:]
		}

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

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

		claims, ok := token.Claims.(*Claims)
		if !ok {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid claims"})
			return
		}

		// Inject claims into context for handlers
		c.Set("user_id", claims.UserID)
		c.Next()
	}
}

2. Generate a Token and Protect a Route

func generateToken(userID string) string {
	claims := &Claims{
		UserID: userID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtKey)
}

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

	// Public route to get a token
	r.GET("/login", func(c *gin.Context) {
		token := generateToken("user-123")
		c.JSON(http.StatusOK, gin.H{"token": token})
	})

	// Protected route
	r.GET("/profile", AuthMiddleware(), func(c *gin.Context) {
		userID, _ := c.Get("user_id")
		c.JSON(http.StatusOK, gin.H{"message": "Hello", "user_id": userID})
	})

	r.Run(":8080")
}

For Echo, the logic is identical but uses echo.Context and echo.MiddlewareFunc. You can use the github.com/labstack/echo-jwt middleware to handle parsing automatically. For Chi, since it lacks built-in JWT support, you must write the middleware manually as shown above, using chi.Middleware and r.Context() to pass data between handlers. Always store the secret key in environment variables, never hardcode it, and ensure you check ExpiresAt to prevent replay attacks.