Echo JWT

authentication-libraryGoEchoJWTmiddlewaresecurity

Authentication Library

Echo JWT

Overview

Echo JWT is a specialized JWT authentication middleware designed exclusively for Go's Echo framework. As of 2025, following security vulnerability CVE-2024-51744, it has been separated from the Echo core and is now provided as an independent package. It uses golang-jwt/jwt/v5 as its internal implementation, providing secure and efficient JWT authentication functionality for Echo applications. The library maintains compatibility with Echo v4 while addressing the latest security requirements.

Details

Echo JWT is a JWT authentication middleware that integrates seamlessly with the Echo framework. Key features include:

  • Echo Native Integration: Full compatibility and optimized integration with Echo v4
  • JWT v5 Support: Secure implementation using the latest golang-jwt/jwt/v5
  • Automatic Validation: Automatic token extraction and validation from Authorization headers
  • Flexible Configuration: Customizable signing keys, algorithms, and extraction methods
  • Error Handling: Appropriate HTTP responses for invalid tokens
  • Claims Access: Easy access to verified JWT claims

Pros and Cons

Pros

  • Native and efficient integration with Echo framework
  • Rapid response and updates to security vulnerabilities
  • Lightweight and high-performance JWT authentication functionality
  • Simple API with easy configuration and customization
  • Idiomatic Go code style
  • Maintains full compatibility with Echo v4

Cons

  • Exclusive to Echo framework, cannot be used with other frameworks
  • Increased dependencies due to being a separate package
  • Potential type assertion issues between golang-jwt/jwt versions
  • Functionality limited to basic JWT authentication
  • Complex authentication flows require additional implementation

Reference Pages

Code Examples

Basic Installation and Setup

# Install Echo JWT middleware
go get github.com/labstack/echo-jwt/v4
go get github.com/golang-jwt/jwt/v5

# Packages for Echo v4
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware

Basic JWT Authentication Setup

package main

import (
    "net/http"
    "time"

    "github.com/golang-jwt/jwt/v5"
    echojwt "github.com/labstack/echo-jwt/v4"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

type jwtCustomClaims struct {
    UserID   int    `json:"user_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

var jwtSecret = []byte("your-secret-key")

func main() {
    e := echo.New()

    // Basic middleware
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())

    // Public routes
    e.POST("/login", login)
    e.POST("/register", register)
    e.GET("/public", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "message": "Public endpoint",
        })
    })

    // JWT protected route group
    protected := e.Group("/api")
    protected.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: jwtSecret,
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }))

    protected.GET("/profile", getProfile)
    protected.PUT("/profile", updateProfile)
    protected.GET("/admin", adminOnly)

    e.Logger.Fatal(e.Start(":8080"))
}

func login(c echo.Context) error {
    type LoginRequest struct {
        Username string `json:"username" validate:"required"`
        Password string `json:"password" validate:"required"`
    }

    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "Invalid request format",
        })
    }

    // User authentication (use proper authentication logic in real implementation)
    if req.Username != "admin" || req.Password != "password" {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "Authentication failed",
        })
    }

    // Generate JWT token
    claims := &jwtCustomClaims{
        UserID:   1,
        Username: req.Username,
        Role:     "admin",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    "echo-jwt-app",
            Subject:   "user-authentication",
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(jwtSecret)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "Failed to generate token",
        })
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "message": "Login successful",
        "token":   tokenString,
        "user": map[string]interface{}{
            "id":       claims.UserID,
            "username": claims.Username,
            "role":     claims.Role,
        },
    })
}

Protected Endpoint Implementation

func getProfile(c echo.Context) error {
    // Get user information from JWT token
    token, ok := c.Get("user").(*jwt.Token)
    if !ok {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "JWT token not found",
        })
    }

    claims, ok := token.Claims.(*jwtCustomClaims)
    if !ok {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "Invalid claims format",
        })
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "user_id":  claims.UserID,
        "username": claims.Username,
        "role":     claims.Role,
        "issued_at": claims.IssuedAt,
        "expires_at": claims.ExpiresAt,
    })
}

func updateProfile(c echo.Context) error {
    token := c.Get("user").(*jwt.Token)
    claims := token.Claims.(*jwtCustomClaims)

    type UpdateProfileRequest struct {
        Email    string `json:"email"`
        FullName string `json:"full_name"`
    }

    var req UpdateProfileRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "Invalid request format",
        })
    }

    // Profile update logic (database operations, etc.)
    // userService.UpdateProfile(claims.UserID, req)

    return c.JSON(http.StatusOK, map[string]interface{}{
        "message": "Profile updated successfully",
        "user_id": claims.UserID,
        "updated_fields": req,
    })
}

Role-Based Access Control (RBAC)

// Custom middleware: Role verification
func requireRole(allowedRoles ...string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            token := c.Get("user").(*jwt.Token)
            claims := token.Claims.(*jwtCustomClaims)

            for _, role := range allowedRoles {
                if claims.Role == role {
                    return next(c)
                }
            }

            return c.JSON(http.StatusForbidden, map[string]string{
                "error": "Access denied to this resource",
            })
        }
    }
}

func adminOnly(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Admin-only endpoint",
    })
}

// Usage in route groups
func setupRoutes(e *echo.Echo) {
    // Admin-only routes
    admin := e.Group("/admin")
    admin.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: jwtSecret,
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }))
    admin.Use(requireRole("admin"))
    admin.GET("/users", adminOnly)
    admin.DELETE("/users/:id", deleteUser)

    // Regular user routes
    user := e.Group("/user")
    user.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: jwtSecret,
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }))
    user.Use(requireRole("user", "admin"))
    user.GET("/dashboard", userDashboard)
}

Advanced Configuration and Customization

func advancedJWTConfig() echojwt.Config {
    return echojwt.Config{
        // Signing key (recommended to get from environment variables)
        SigningKey: []byte("your-secret-key"),

        // Specify signing algorithm
        SigningMethod: "HS256",

        // Claims creation function
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },

        // Customize token extraction method
        TokenLookup: "header:Authorization:Bearer ,cookie:token,query:token",

        // Error handler for authentication failures
        ErrorHandler: func(c echo.Context, err error) error {
            return c.JSON(http.StatusUnauthorized, map[string]interface{}{
                "error": "Authentication failed",
                "details": err.Error(),
                "timestamp": time.Now().Unix(),
            })
        },

        // Before hook: processing before token validation
        BeforeFunc: func(c echo.Context) {
            // Request logging, metrics collection, etc.
            c.Logger().Info("Starting JWT authentication")
        },

        // Success hook: processing after successful authentication
        SuccessHandler: func(c echo.Context) {
            token := c.Get("user").(*jwt.Token)
            claims := token.Claims.(*jwtCustomClaims)
            c.Logger().Infof("User %s authenticated", claims.Username)
        },

        // KeyFunc: support for multiple signing keys
        KeyFunc: func(t *jwt.Token) (interface{}, error) {
            // Return appropriate key based on token header
            if keyID, ok := t.Header["kid"].(string); ok {
                return getKeyByID(keyID), nil
            }
            return jwtSecret, nil
        },
    }
}

func getKeyByID(keyID string) []byte {
    // Return appropriate signing key based on key ID
    keys := map[string][]byte{
        "key1": []byte("secret-key-1"),
        "key2": []byte("secret-key-2"),
    }
    
    if key, exists := keys[keyID]; exists {
        return key
    }
    return jwtSecret // Default key
}

Refresh Token Implementation

type RefreshTokenClaims struct {
    UserID int    `json:"user_id"`
    Type   string `json:"type"` // "refresh"
    jwt.RegisteredClaims
}

func generateTokenPair(userID int, username, role string) (string, string, error) {
    // Access token (short-term)
    accessClaims := &jwtCustomClaims{
        UserID:   userID,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 15)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "echo-jwt-app",
        },
    }

    accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
    accessTokenString, err := accessToken.SignedString(jwtSecret)
    if err != nil {
        return "", "", err
    }

    // Refresh token (long-term)
    refreshClaims := &RefreshTokenClaims{
        UserID: userID,
        Type:   "refresh",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)), // 1 week
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "echo-jwt-app",
        },
    }

    refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
    refreshTokenString, err := refreshToken.SignedString(jwtSecret)
    if err != nil {
        return "", "", err
    }

    return accessTokenString, refreshTokenString, nil
}

func refreshToken(c echo.Context) error {
    type RefreshRequest struct {
        RefreshToken string `json:"refresh_token"`
    }

    var req RefreshRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "Invalid request format",
        })
    }

    // Validate refresh token
    token, err := jwt.ParseWithClaims(req.RefreshToken, &RefreshTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })

    if err != nil || !token.Valid {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "Invalid refresh token",
        })
    }

    claims := token.Claims.(*RefreshTokenClaims)
    if claims.Type != "refresh" {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "Invalid token type",
        })
    }

    // Generate new access token
    // Get user information (from database)
    user := getUserByID(claims.UserID) // Implementation required

    accessToken, _, err := generateTokenPair(user.ID, user.Username, user.Role)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "Failed to generate token",
        })
    }

    return c.JSON(http.StatusOK, map[string]string{
        "access_token": accessToken,
        "token_type":   "Bearer",
    })
}