Echo Route Protection
Introduction
Route protection is a critical aspect of web application security. It ensures that only authorized users can access specific resources or functionality within your application. In the Echo framework, route protection is implemented through middleware that intercepts requests before they reach the handler functions.
This guide will walk you through implementing robust route protection in your Echo applications. You'll learn how to restrict access to routes based on user roles, authentication status, and custom conditions.
Understanding Route Protection Basics
Route protection in Echo works on a simple principle: intercept the request using middleware, verify if the user has the necessary permissions, and then either allow the request to proceed or return an error response.
Why Route Protection Matters
Without proper route protection:
- Unauthorized users could access sensitive information
- Users might perform actions they shouldn't be allowed to
- Your application becomes vulnerable to security breaches
Implementing Basic Authentication Protection
Let's start with a simple authentication check that verifies if a user is logged in before allowing access to a protected route.
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
// Public routes
e.GET("/", publicHandler)
e.GET("/login", loginHandler)
// Protected group
protectedGroup := e.Group("/api")
// Apply the authentication middleware to the group
protectedGroup.Use(authMiddleware)
// Routes in this group will require authentication
protectedGroup.GET("/profile", profileHandler)
protectedGroup.GET("/dashboard", dashboardHandler)
e.Logger.Fatal(e.Start(":8080"))
}
// Middleware to check if user is authenticated
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the token from the request header
token := c.Request().Header.Get("Authorization")
// In a real application, you would validate the token
// For this example, we'll just check if it's not empty
if token == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authentication required",
})
}
// If token is valid, proceed to the next handler
return next(c)
}
}
func publicHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is a public page")
}
func loginHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is the login page")
}
func profileHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is your profile page")
}
func dashboardHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is your dashboard")
}
In this example:
/
and/login
are public routes accessible to everyone- All routes under
/api
require authentication - The
authMiddleware
checks for the presence of an Authorization header - If the header is missing, it returns a 401 Unauthorized status
Role-Based Access Control
Most applications need more granular control than just "authenticated" or "not authenticated." Let's implement role-based access control (RBAC) to restrict certain routes to users with specific roles.
package main
import (
"github.com/labstack/echo/v4"
"net/http"
"strings"
)
// Define user roles
const (
RoleUser = "user"
RoleAdmin = "admin"
)
func main() {
e := echo.New()
// Public routes
e.GET("/", publicHandler)
e.GET("/login", loginHandler)
// User routes - requires user role
userGroup := e.Group("/user")
userGroup.Use(authMiddleware)
userGroup.Use(roleMiddleware(RoleUser))
userGroup.GET("/profile", profileHandler)
// Admin routes - requires admin role
adminGroup := e.Group("/admin")
adminGroup.Use(authMiddleware)
adminGroup.Use(roleMiddleware(RoleAdmin))
adminGroup.GET("/dashboard", adminDashboardHandler)
adminGroup.GET("/users", manageUsersHandler)
e.Logger.Fatal(e.Start(":8080"))
}
// Middleware to check if user is authenticated
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authentication required",
})
}
// In a real application, you would decode and validate the token
// For this example, we'll simulate assigning a role based on the token
// Let's assume tokens starting with "admin_" are for admin users
// and tokens starting with "user_" are for regular users
if strings.HasPrefix(token, "admin_") {
c.Set("userRole", RoleAdmin)
} else if strings.HasPrefix(token, "user_") {
c.Set("userRole", RoleUser)
} else {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid token",
})
}
return next(c)
}
}
// Middleware to check if user has the required role
func roleMiddleware(requiredRole string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
userRole := c.Get("userRole")
// If the user is an admin, they can access everything
if userRole == RoleAdmin {
return next(c)
}
// For other roles, check if they match the required role
if userRole != requiredRole {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Access denied: insufficient permissions",
})
}
return next(c)
}
}
}
// Handlers for the routes
func publicHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is a public page")
}
func loginHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is the login page")
}
func profileHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is your profile page")
}
func adminDashboardHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is the admin dashboard")
}
func manageUsersHandler(c echo.Context) error {
return c.String(http.StatusOK, "Here you can manage users")
}
In this example:
- We define two roles:
user
andadmin
- The
authMiddleware
now extracts role information from the token - The
roleMiddleware
checks if a user has the required role - The
/user/*
routes require at least a user role - The
/admin/*
routes require an admin role - Admins can access all routes (common pattern where higher roles include lower role permissions)
JWT-Based Route Protection
For a more practical and secure implementation, let's use JWT (JSON Web Tokens) to protect our routes. Echo provides middleware for JWT validation out of the box.
package main
import (
"github.com/golang-jwt/jwt"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"time"
)
// JWT secret key
var jwtSecret = []byte("your-secret-key-here")
// User claim structure
type jwtCustomClaims struct {
Name string `json:"name"`
Role string `json:"role"`
jwt.StandardClaims
}
func main() {
e := echo.New()
// Public routes
e.GET("/", publicHandler)
e.POST("/login", loginHandler)
// Restricted group
r := e.Group("/api")
// Configure JWT middleware
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: jwtSecret,
}
// Apply JWT middleware
r.Use(middleware.JWTWithConfig(config))
// User routes
r.GET("/user/profile", profileHandler)
// Admin routes with additional middleware check
r.GET("/admin/dashboard", adminDashboardHandler, checkAdmin)
e.Logger.Fatal(e.Start(":8080"))
}
// Login handler
func loginHandler(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Check credentials (in a real app, check against database)
if username == "admin" && password == "admin123" {
return generateToken(c, username, "admin")
} else if username == "user" && password == "user123" {
return generateToken(c, username, "user")
}
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}
// Generate JWT token
func generateToken(c echo.Context, username string, role string) error {
// Set expiration time
expiration := time.Now().Add(time.Hour * 24)
// Set custom claims
claims := &jwtCustomClaims{
username,
role,
jwt.StandardClaims{
ExpiresAt: expiration.Unix(),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token
t, err := token.SignedString(jwtSecret)
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
// Admin middleware
func checkAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
if claims.Role != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Admin access required",
})
}
return next(c)
}
}
// Route handlers
func publicHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is a public page")
}
func profileHandler(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome to your profile, "+name)
}
func adminDashboardHandler(c echo.Context) error {
return c.String(http.StatusOK, "This is the admin dashboard")
}
In this example:
- We use Echo's built-in JWT middleware
- We create a custom claims structure that includes role information
- We establish a
/login
route that generates JWT tokens - All routes under
/api
require a valid JWT token - The
/api/admin/*
routes have an additional middleware to check for admin role - The handlers can access the user's information from the JWT token
Testing Your Protected Routes
To test your protected routes, you can use tools like cURL or Postman. Here's how you would test the JWT-based route protection:
- First, obtain a token by sending a login request:
curl -X POST -d "username=admin&password=admin123" http://localhost:8080/login
This will return a JSON response with a token:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
- Use the token to access a protected route:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:8080/api/admin/dashboard
If the token is valid and has the admin role, you'll see:
This is the admin dashboard
Context-Based Permission Checking
Sometimes, you need more complex permission logic that depends on the request context. For example, users should only access their own resources. Here's how to implement context-based permission checking:
// Middleware to check if a user can access a specific resource
func canAccessResourceMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the user ID from the token
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
userID := claims.Subject // Assuming subject contains the user ID
// Get the resource ID from the URL parameter
resourceID := c.Param("id")
// In a real app, you would check if this user owns this resource
// or has permission to access it (e.g., by querying the database)
if !userCanAccessResource(userID, resourceID) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "You don't have permission to access this resource",
})
}
return next(c)
}
}
// Example function to check resource access (in a real app, this would query a database)
func userCanAccessResource(userID, resourceID string) bool {
// Simulate database check
// Return true if the user owns the resource or has permission
// This is a placeholder for your actual logic
return userOwnsResource(userID, resourceID) || userHasPermission(userID, resourceID)
}
Best Practices for Route Protection
-
Use HTTPS: Always use HTTPS in production to protect tokens and credentials during transmission.
-
Secure JWT Secrets: Use strong, unique JWT secrets and consider using environment variables to store them.
-
Token Expiration: Set appropriate expiration times for tokens. Short-lived tokens are more secure.
-
Refresh Tokens: Implement refresh token mechanism for better security while maintaining good UX.
-
Rate Limiting: Add rate limiting to prevent brute force attacks:
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Apply rate limiting middleware
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
-
Proper Error Messages: Don't reveal too much information in error messages.
-
Logging: Log authentication failures to monitor for potential attacks:
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// ... authentication logic
if authFailed {
// Log the failure with useful context (but avoid logging sensitive data)
c.Logger().Warnf("Authentication failed: IP=%s, User-Agent=%s",
c.RealIP(), c.Request().UserAgent())
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authentication failed",
})
}
return next(c)
}
}
Summary
In this guide, we've explored several approaches to implementing route protection in Echo:
- Basic authentication middleware that checks for the presence of credentials
- Role-based access control (RBAC) for more granular permissions
- JWT-based authentication with Echo's built-in middleware
- Context-based permission checking for resource-specific access control
By combining these techniques, you can build a robust security system for your Echo application that protects sensitive routes and resources while allowing appropriate access to authorized users.
Additional Resources and Exercises
Resources
Exercises
-
User Management System: Build a simple user management system with registration, login, and protected routes for user profiles.
-
Resource Ownership: Implement a blog system where users can create posts, but only edit or delete their own posts.
-
Permission Levels: Create an application with at least three different permission levels (e.g., guest, user, moderator, admin) with appropriate route protections.
-
Token Blacklisting: Implement a token blacklist to immediately invalidate tokens upon user logout.
-
Two-Factor Authentication: Add an optional two-factor authentication layer for even more security on critical routes.
By practicing these exercises, you'll gain hands-on experience with Echo route protection and develop a deeper understanding of web application security principles.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)