Skip to main content

Echo Authorization Rules

Introduction

Authorization rules determine who can access specific resources in your web application. Unlike authentication which verifies user identity, authorization controls what authenticated users are allowed to do. In the Echo framework, you can implement custom authorization rules to secure your API endpoints based on user roles, permissions, or other criteria.

This guide will walk you through implementing authorization rules in Echo applications, from basic role-based access control to more complex permission systems.

Basic Authorization Concepts

Before diving into code, let's understand the key concepts:

  • Authorization: Determining if a user has permission to access a resource or perform an action
  • Rules: Conditions that define who can access what
  • Middleware: Echo's way of processing requests before they reach route handlers
  • Claims: User information (often from JWT tokens) containing role/permission data

Implementing Basic Role-Based Authorization

The simplest form of authorization is role-based access control (RBAC). Let's create a middleware that checks if a user has the required role.

Step 1: Define a middleware function

go
package middleware

import (
"github.com/labstack/echo/v4"
"net/http"
)

// RoleAuth middleware checks if user has required role
func RoleAuth(requiredRole string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user from context (assuming you've set it in authentication middleware)
user := c.Get("user")
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

// Type assertion to get user data
userData := user.(map[string]interface{})

// Check if user has required role
userRole, ok := userData["role"].(string)
if !ok || userRole != requiredRole {
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}

return next(c)
}
}
}

Step 2: Apply the middleware to your routes

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"your-app/middleware" // Import your custom middleware
)

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

// Apply JWT middleware to authenticate users
e.Use(middleware.JWT([]byte("secret")))

// Public endpoint - no authorization required
e.GET("/public", func(c echo.Context) error {
return c.String(http.StatusOK, "Public content")
})

// Admin-only endpoint
e.GET("/admin", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin content")
}, middleware.RoleAuth("admin"))

// User-level endpoint
e.GET("/user-profile", func(c echo.Context) error {
return c.String(http.StatusOK, "User profile")
}, middleware.RoleAuth("user"))

e.Start(":8080")
}

Advanced Authorization Rules

For more complex applications, simple role-based authorization may not be enough. Let's implement a more sophisticated permission system.

Permission-Based Authorization

go
package middleware

import (
"github.com/labstack/echo/v4"
"net/http"
)

// PermissionAuth middleware checks if user has required permissions
func PermissionAuth(requiredPermissions ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user")
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

userData := user.(map[string]interface{})

// Get user permissions
userPermissionsRaw, ok := userData["permissions"].([]interface{})
if !ok {
return echo.NewHTTPError(http.StatusForbidden, "No permissions defined")
}

// Convert to string slice
userPermissions := make([]string, len(userPermissionsRaw))
for i, p := range userPermissionsRaw {
userPermissions[i] = p.(string)
}

// Check if user has all required permissions
for _, requiredPerm := range requiredPermissions {
hasPermission := false
for _, userPerm := range userPermissions {
if userPerm == requiredPerm {
hasPermission = true
break
}
}

if !hasPermission {
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}
}

return next(c)
}
}
}

Using Permission-Based Authorization

go
// Article endpoints with permission checks
articlesGroup := e.Group("/articles")
{
// Anyone can read articles
articlesGroup.GET("", listArticlesHandler)

// Only users with "articles:create" permission can create articles
articlesGroup.POST("", createArticleHandler, middleware.PermissionAuth("articles:create"))

// Only users with "articles:edit" permission can update articles
articlesGroup.PUT("/:id", updateArticleHandler, middleware.PermissionAuth("articles:edit"))

// Need both permissions to delete
articlesGroup.DELETE("/:id", deleteArticleHandler,
middleware.PermissionAuth("articles:delete", "articles:admin"))
}

Resource-Based Authorization

Sometimes you need to check if a user can access a specific resource, like an article they authored. Let's implement resource-based authorization:

go
// ArticleOwnerAuth middleware checks if user owns the article
func ArticleOwnerAuth(articleService *services.ArticleService) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get article ID from URL parameter
articleID := c.Param("id")

// Get user from context
user := c.Get("user")
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

userData := user.(map[string]interface{})
userID, ok := userData["id"].(string)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "Invalid user data")
}

// Fetch article from database
article, err := articleService.GetByID(articleID)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "Article not found")
}

// Check if user is the author
if article.AuthorID != userID {
// Check if user is an admin as a fallback
userRole, ok := userData["role"].(string)
if !ok || userRole != "admin" {
return echo.NewHTTPError(http.StatusForbidden, "You don't have permission to access this article")
}
}

// Store article in context for later use
c.Set("article", article)

return next(c)
}
}
}

Using Resource-Based Authorization

go
// Route that checks resource ownership
e.PUT("/articles/:id", updateArticleHandler, middleware.ArticleOwnerAuth(articleService))
e.DELETE("/articles/:id", deleteArticleHandler, middleware.ArticleOwnerAuth(articleService))

Combining Multiple Authorization Rules

For complex authorization scenarios, you might need to combine different rules:

go
// Define a custom middleware that combines multiple checks
func AdminOrOwnerAuth(articleService *services.ArticleService) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(map[string]interface{})

// Check if admin
if role, ok := user["role"].(string); ok && role == "admin" {
return next(c) // Admins have automatic access
}

// Not an admin, check ownership
articleID := c.Param("id")
userID := user["id"].(string)

article, err := articleService.GetByID(articleID)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "Article not found")
}

if article.AuthorID != userID {
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}

c.Set("article", article)
return next(c)
}
}
}

Real-World Example: Blog API with Authorization Rules

Let's see how these concepts come together in a blog application:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"your-blog/middlewares"
"your-blog/services"
)

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

// Initialize services
articleService := services.NewArticleService()

// Global middlewares
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Authentication middleware (applies to routes that need it)
authMiddleware := middleware.JWT([]byte("your-secret-key"))

// Public routes
e.GET("/articles", listArticlesHandler)
e.GET("/articles/:id", getArticleHandler)

// Auth required routes
auth := e.Group("")
auth.Use(authMiddleware)

// Regular user routes - only need to be logged in
auth.GET("/profile", getProfileHandler)
auth.PUT("/profile", updateProfileHandler)

// Article management - needs specific permissions
auth.POST("/articles", createArticleHandler, middlewares.PermissionAuth("articles:create"))

// Update - user must be owner or admin
auth.PUT("/articles/:id", updateArticleHandler, middlewares.AdminOrOwnerAuth(articleService))

// Delete - user must be owner or have delete permission
auth.DELETE("/articles/:id", deleteArticleHandler, middlewares.CombineMiddlewares(
middlewares.AdminOrOwnerAuth(articleService),
middlewares.PermissionAuth("articles:delete"),
))

// Admin-only routes
admin := e.Group("/admin")
admin.Use(authMiddleware, middlewares.RoleAuth("admin"))

admin.GET("/users", listUsersHandler)
admin.DELETE("/users/:id", deleteUserHandler)

e.Start(":8080")
}

Testing Authorization Rules

To ensure your authorization rules work correctly, write tests for different scenarios:

go
func TestAdminAccess(t *testing.T) {
// Setup Echo
e := echo.New()

// Setup route with admin-only middleware
e.GET("/admin-resource", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin content")
}, middleware.RoleAuth("admin"))

// Create admin test request
req := httptest.NewRequest(http.MethodGet, "/admin-resource", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Mock authenticated admin user
c.Set("user", map[string]interface{}{
"id": "123",
"role": "admin",
})

// Test handler with admin context
h := middleware.RoleAuth("admin")(func(c echo.Context) error {
return c.String(http.StatusOK, "Admin content")
})

// Execute request
if err := h(c); err != nil {
t.Errorf("Expected no error for admin access, got %v", err)
}

// Check response
if rec.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, rec.Code)
}
}

func TestForbiddenAccess(t *testing.T) {
// Setup Echo
e := echo.New()

// Setup route with admin-only middleware
e.GET("/admin-resource", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin content")
}, middleware.RoleAuth("admin"))

// Create regular user test request
req := httptest.NewRequest(http.MethodGet, "/admin-resource", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Mock authenticated regular user
c.Set("user", map[string]interface{}{
"id": "456",
"role": "user",
})

// Test handler with user context
h := middleware.RoleAuth("admin")(func(c echo.Context) error {
return c.String(http.StatusOK, "Admin content")
})

// Execute request - should fail with forbidden error
err := h(c)
httpErr, ok := err.(*echo.HTTPError)

// Verify error
if !ok {
t.Error("Expected HTTPError for unauthorized access")
}

if httpErr.Code != http.StatusForbidden {
t.Errorf("Expected status code %d, got %d", http.StatusForbidden, httpErr.Code)
}
}

Best Practices for Authorization Rules

  1. Defense in depth: Don't rely solely on middleware authorization; validate permissions in handlers too.
  2. Least privilege: Grant the minimum permissions needed for each role.
  3. Fail closed: Default to denying access if authorization check fails.
  4. Log access attempts: Log failed authorization attempts for security monitoring.
  5. Consistent error responses: Don't reveal why authorization failed in detail (prevents information leakage).
  6. Cache permissions: For complex permission systems, cache permission data to reduce database queries.

Here's how to implement some of these best practices:

go
// Handler with additional validation
func updateArticleHandler(c echo.Context) error {
// Get article from context (set by middleware)
article, ok := c.Get("article").(*models.Article)
if !ok {
// Defensive check - shouldn't happen if middleware worked correctly
return echo.NewHTTPError(http.StatusInternalServerError, "Article data missing")
}

// Get user data
user := c.Get("user").(map[string]interface{})
userID := user["id"].(string)

// Double-check permissions even though middleware should have handled it
if article.AuthorID != userID {
// Check if admin
role, ok := user["role"].(string)
if !ok || role != "admin" {
return echo.NewHTTPError(http.StatusForbidden, "Unauthorized")
}
}

// Continue with update...
// [article update logic]

return c.JSON(http.StatusOK, article)
}

Summary

Authorization rules in Echo allow you to control access to your application resources based on user roles, permissions, and resource ownership. We've covered:

  1. Basic role-based authorization: Restricting access based on user roles
  2. Permission-based authorization: Fine-grained access control using specific permissions
  3. Resource-based authorization: Ensuring users can only access resources they own
  4. Combining authorization rules: Creating complex access control logic
  5. Testing authorization rules: Verifying your authorization logic works as expected
  6. Best practices: Guidelines for implementing secure authorization

By implementing these authorization patterns, you can create secure Echo applications that protect sensitive resources and operations while providing appropriate access to authorized users.

Additional Resources and Exercises

Further Reading

Exercises

  1. Basic Role-Based Auth: Create a simple API with public, user, and admin endpoints using role-based authorization.
  2. Permission System: Implement a permission-based system where users can have multiple permissions.
  3. Resource Ownership: Build a blog API where users can only edit their own posts (unless they're admins).
  4. Complex Rules: Create an e-commerce API where shop owners can manage their products, admins can manage all products, and customers can only view products.
  5. Rate Limiting: Enhance your authorization system with rate limiting based on user roles (e.g., free users have stricter limits than premium users).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)