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.