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
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
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
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
// 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:
// 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
// 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:
// 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:
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:
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
- Defense in depth: Don't rely solely on middleware authorization; validate permissions in handlers too.
- Least privilege: Grant the minimum permissions needed for each role.
- Fail closed: Default to denying access if authorization check fails.
- Log access attempts: Log failed authorization attempts for security monitoring.
- Consistent error responses: Don't reveal why authorization failed in detail (prevents information leakage).
- Cache permissions: For complex permission systems, cache permission data to reduce database queries.
Here's how to implement some of these best practices:
// 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:
- Basic role-based authorization: Restricting access based on user roles
- Permission-based authorization: Fine-grained access control using specific permissions
- Resource-based authorization: Ensuring users can only access resources they own
- Combining authorization rules: Creating complex access control logic
- Testing authorization rules: Verifying your authorization logic works as expected
- 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
- Basic Role-Based Auth: Create a simple API with public, user, and admin endpoints using role-based authorization.
- Permission System: Implement a permission-based system where users can have multiple permissions.
- Resource Ownership: Build a blog API where users can only edit their own posts (unless they're admins).
- 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.
- 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! :)