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,
})
})
Summary
Authentication middleware in Echo provides a powerful way to secure your web applications and APIs. In this tutorial, we've covered:
- Basic authentication middleware
- JWT authentication middleware
- Custom API key authentication
- Role-based access control
These authentication methods can be combined with other Echo middleware like rate limiting, CORS, and request logging to create a comprehensive security solution for your applications.
Remember that security is a complex topic, and authentication is just one part of a comprehensive security strategy. Always keep your dependencies updated and follow security best practices.
Additional Resources
- Echo Framework Documentation
- JWT.io - Learn more about JWT tokens
- OWASP Authentication Cheat Sheet
Exercises
- Implement a middleware that checks both JWT authentication and API key for extra security.
- Create a middleware that validates token expiration and automatically rejects expired tokens.
- Implement a logout functionality that invalidates tokens.
- Add rate limiting to the login route to prevent brute force attacks.
- Set up a database to store user credentials and authenticate against it instead of using hardcoded values.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)