Echo Authentication Middleware
Authentication is a critical aspect of web application security. It ensures that only legitimate users have access to protected resources. In the Echo framework, we can implement authentication through middleware - special functions that process requests before they reach route handlers. This tutorial will walk you through implementing and using authentication middleware in Echo.
Introduction to Echo Authentication Middleware
Authentication middleware in Echo intercepts incoming HTTP requests, validates the user's identity, and either allows the request to proceed to the handler or rejects it with an appropriate error response. This middleware sits between your client and your route handlers, acting as a gatekeeper for your protected resources.

Basic Authentication Middleware
Let's start with a simple username/password authentication middleware using Echo's built-in BasicAuth middleware.
package main
import (
    "net/http"
    
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)
func main() {
    e := echo.New()
    
    // Define credentials
    credentials := make(map[string]string)
    credentials["john"] = "secret123"
    credentials["jane"] = "password456"
    
    // BasicAuth middleware
    e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
        // Check if username exists and password matches
        if storedPassword, ok := credentials[username]; ok && storedPassword == password {
            // Store authenticated user in context
            c.Set("user", username)
            return true, nil
        }
        return false, nil
    }))
    
    // Protected route
    e.GET("/protected", func(c echo.Context) error {
        user := c.Get("user").(string)
        return c.String(http.StatusOK, "Welcome "+user+"!")
    })
    
    e.Start(":8080")
}
When you access /protected, the browser will prompt you for credentials. If you enter "john" as username and "secret123" as password, you'll see:
Welcome john!
If your credentials are incorrect, you'll receive a 401 Unauthorized response.
JWT Authentication Middleware
Basic authentication is simple but not suitable for modern web applications or APIs. JWT (JSON Web Token) authentication is more secure and scalable.
First, install the JWT middleware package:
go get github.com/labstack/echo-jwt/v4
Now, let's implement JWT authentication:
package main
import (
    "net/http"
    "time"
    
    "github.com/labstack/echo/v4"
    "github.com/golang-jwt/jwt/v4"
    echojwt "github.com/labstack/echo-jwt/v4"
)
// User represents the user information
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}
// jwtCustomClaims are custom claims extending default ones
type jwtCustomClaims struct {
    Username string `json:"username"`
    Admin    bool   `json:"admin"`
    jwt.RegisteredClaims
}
func main() {
    e := echo.New()
    
    // Secret key for signing JWT tokens
    secretKey := []byte("your-secret-key")
    
    // Login route to generate JWT token
    e.POST("/login", func(c echo.Context) error {
        username := c.FormValue("username")
        password := c.FormValue("password")
        
        // In a real application, verify against database
        if username != "john" || password != "secret123" {
            return echo.ErrUnauthorized
        }
        
        // Set claims
        claims := &jwtCustomClaims{
            Username: username,
            Admin:    username == "admin",
            RegisteredClaims: jwt.RegisteredClaims{
                ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
            },
        }
        
        // Create token with claims
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
        
        // Generate encoded token
        t, err := token.SignedString(secretKey)
        if err != nil {
            return err
        }
        
        return c.JSON(http.StatusOK, map[string]string{
            "token": t,
        })
    })
    
    // Restricted group
    r := e.Group("/restricted")
    
    // Configure JWT middleware
    config := echojwt.Config{
        SigningKey:    secretKey,
        SigningMethod: "HS256",
    }
    
    // Apply JWT middleware to restricted group
    r.Use(echojwt.WithConfig(config))
    
    // Protected route
    r.GET("", func(c echo.Context) error {
        // Get user from token
        token := c.Get("user").(*jwt.Token)
        claims := token.Claims.(*jwtCustomClaims)
        
        return c.String(http.StatusOK, "Welcome "+claims.Username+"!")
    })
    
    e.Start(":8080")
}
To test this JWT authentication:
- First, obtain a token by making a POST request to /login:
curl -X POST -d "username=john&password=secret123" http://localhost:8080/login
You'll receive a response like:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
- Use this token to access the restricted route:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:8080/restricted
If the token is valid, you'll see:
Welcome john!
Custom Authentication Middleware
Sometimes you may need to implement a custom authentication scheme. Here's an example of a custom API key middleware:
package main
import (
    "net/http"
    
    "github.com/labstack/echo/v4"
)
// APIKeyAuth middleware
func APIKeyAuth(apiKeys map[string]string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Get API key from header
            key := c.Request().Header.Get("X-API-Key")
            
            // Check if API key exists
            if key == "" {
                return echo.NewHTTPError(http.StatusUnauthorized, "Missing API key")
            }
            
            // Validate API key
            if username, valid := apiKeys[key]; valid {
                // Store user in context
                c.Set("user", username)
                return next(c)
            }
            
            return echo.NewHTTPError(http.StatusUnauthorized, "Invalid API key")
        }
    }
}
func main() {
    e := echo.New()
    
    // Define API keys (key -> username)
    apiKeys := map[string]string{
        "abc123": "john",
        "def456": "jane",
    }
    
    // API key auth middleware for specific group
    api := e.Group("/api")
    api.Use(APIKeyAuth(apiKeys))
    
    // Protected API route
    api.GET("/data", func(c echo.Context) error {
        user := c.Get("user").(string)
        return c.JSON(http.StatusOK, map[string]string{
            "message": "Hello, " + user + "!",
            "data":    "This is protected data",
        })
    })
    
    e.Start(":8080")
}
To test this API key authentication:
curl -H "X-API-Key: abc123" http://localhost:8080/api/data
You should receive:
{"message":"Hello, john!","data":"This is protected data"}
If you use an invalid API key or no key at all, you'll get a 401 Unauthorized error.
Role-Based Access Control
Let's extend our JWT example to implement role-based access control:
package main
import (
    "net/http"
    "time"
    
    "github.com/labstack/echo/v4"
    "github.com/golang-jwt/jwt/v4"
    echojwt "github.com/labstack/echo-jwt/v4"
)
type jwtCustomClaims struct {
    Username string   `json:"username"`
    Roles    []string `json:"roles"`
    jwt.RegisteredClaims
}
// RequireRole middleware checks if user has required role
func RequireRole(role 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)
            
            // Check if user has the required role
            hasRole := false
            for _, r := range claims.Roles {
                if r == role {
                    hasRole = true
                    break
                }
            }
            
            if !hasRole {
                return echo.ErrForbidden
            }
            
            return next(c)
        }
    }
}
func main() {
    e := echo.New()
    
    secretKey := []byte("your-secret-key")
    
    // Login route with roles
    e.POST("/login", func(c echo.Context) error {
        username := c.FormValue("username")
        password := c.FormValue("password")
        
        // User database simulation
        users := map[string]struct {
            password string
            roles    []string
        }{
            "john":  {"secret123", []string{"user"}},
            "admin": {"admin123", []string{"user", "admin"}},
        }
        
        // Check credentials
        user, exists := users[username]
        if !exists || user.password != password {
            return echo.ErrUnauthorized
        }
        
        // Set claims with roles
        claims := &jwtCustomClaims{
            Username: username,
            Roles:    user.roles,
            RegisteredClaims: jwt.RegisteredClaims{
                ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
            },
        }
        
        // Create token with claims
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
        
        // Generate encoded token
        t, err := token.SignedString(secretKey)
        if err != nil {
            return err
        }
        
        return c.JSON(http.StatusOK, map[string]string{
            "token": t,
        })
    })
    
    // JWT middleware config
    config := echojwt.Config{
        SigningKey:    secretKey,
        SigningMethod: "HS256",
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }
    
    // User routes - require 'user' role
    userRoutes := e.Group("/user")
    userRoutes.Use(echojwt.WithConfig(config))
    userRoutes.Use(RequireRole("user"))
    
    userRoutes.GET("/profile", func(c echo.Context) error {
        token := c.Get("user").(*jwt.Token)
        claims := token.Claims.(*jwtCustomClaims)
        
        return c.String(http.StatusOK, "User profile for "+claims.Username)
    })
    
    // Admin routes - require 'admin' role
    adminRoutes := e.Group("/admin")
    adminRoutes.Use(echojwt.WithConfig(config))
    adminRoutes.Use(RequireRole("admin"))
    
    adminRoutes.GET("/dashboard", func(c echo.Context) error {
        return c.String(http.StatusOK, "Admin Dashboard - Restricted Area")
    })
    
    e.Start(":8080")
}
To test this role-based authentication:
- Login as a regular user:
curl -X POST -d "username=john&password=secret123" http://localhost:8080/login
- Access user profile with the token (should work):
curl -H "Authorization: Bearer TOKEN_HERE" http://localhost:8080/user/profile
- Try to access admin dashboard with the same token (should fail):
curl -H "Authorization: Bearer TOKEN_HERE" http://localhost:8080/admin/dashboard
- Login as admin:
curl -X POST -d "username=admin&password=admin123" http://localhost:8080/login
- Use admin token to access admin dashboard (should work):
curl -H "Authorization: Bearer ADMIN_TOKEN_HERE" http://localhost:8080/admin/dashboard
Best Practices for Authentication Middleware
- 
Don't store sensitive data in JWT tokens: Tokens can be decoded (although not verified without the secret key). 
- 
Set appropriate token expiration: Short-lived tokens are more secure. 
- 
Use HTTPS: Always use HTTPS in production to prevent token interception. 
- 
Store secrets securely: Don't hardcode JWT secret keys in your code. 
- 
Implement token refresh: Allow users to obtain a new token without re-authenticating. 
- 
Add rate limiting: Prevent brute-force attacks by limiting authentication attempts. 
Here's an example of implementing token refresh:
// Token refresh route
e.POST("/refresh", func(c echo.Context) error {
    // Get refresh token from request
    refreshToken := c.FormValue("refresh_token")
    
    // Verify refresh token (in a real app, check against stored refresh tokens)
    // ...
    
    // Generate new access token
    // ...
    
    return c.JSON(http.StatusOK, map[string]string{
        "access_token": newAccessToken,
    })
})