Skip to main content

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.

go
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.

go
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:

  1. We define two roles: user and admin
  2. The authMiddleware now extracts role information from the token
  3. The roleMiddleware checks if a user has the required role
  4. The /user/* routes require at least a user role
  5. The /admin/* routes require an admin role
  6. 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.

go
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:

  1. We use Echo's built-in JWT middleware
  2. We create a custom claims structure that includes role information
  3. We establish a /login route that generates JWT tokens
  4. All routes under /api require a valid JWT token
  5. The /api/admin/* routes have an additional middleware to check for admin role
  6. 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:

  1. First, obtain a token by sending a login request:
bash
curl -X POST -d "username=admin&password=admin123" http://localhost:8080/login

This will return a JSON response with a token:

json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
  1. Use the token to access a protected route:
bash
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:

go
// 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

  1. Use HTTPS: Always use HTTPS in production to protect tokens and credentials during transmission.

  2. Secure JWT Secrets: Use strong, unique JWT secrets and consider using environment variables to store them.

  3. Token Expiration: Set appropriate expiration times for tokens. Short-lived tokens are more secure.

  4. Refresh Tokens: Implement refresh token mechanism for better security while maintaining good UX.

  5. Rate Limiting: Add rate limiting to prevent brute force attacks:

go
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

// Apply rate limiting middleware
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
  1. Proper Error Messages: Don't reveal too much information in error messages.

  2. Logging: Log authentication failures to monitor for potential attacks:

go
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:

  1. Basic authentication middleware that checks for the presence of credentials
  2. Role-based access control (RBAC) for more granular permissions
  3. JWT-based authentication with Echo's built-in middleware
  4. 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

  1. User Management System: Build a simple user management system with registration, login, and protected routes for user profiles.

  2. Resource Ownership: Implement a blog system where users can create posts, but only edit or delete their own posts.

  3. Permission Levels: Create an application with at least three different permission levels (e.g., guest, user, moderator, admin) with appropriate route protections.

  4. Token Blacklisting: Implement a token blacklist to immediately invalidate tokens upon user logout.

  5. 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! :)