Echo Authorization Basics
Authentication verifies who a user is, but authorization determines what they can do. In this guide, we'll explore how to implement authorization in your Echo applications, control access to resources, and secure your routes based on user roles and permissions.
Introduction to Authorization in Echo
Authorization is the process of determining whether an authenticated user has permission to access a particular resource or perform a specific action. Echo doesn't provide built-in authorization mechanisms like some frameworks, but it offers the flexibility to implement custom authorization strategies through middleware.
Key authorization concepts we'll cover:
- Middleware-based authorization
- Role-based access control (RBAC)
- Permission-based authorization
- JWT claims for authorization
Basic Authorization Middleware
Let's start by creating a simple authorization middleware that checks if a user has a specific role:
// isAdmin middleware checks if user has admin role
func isAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*models.User) // Get user from context after authentication
if user == nil || user.Role != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "You don't have access to this resource",
})
}
return next(c)
}
}
To use this middleware on routes that should only be accessible by admins:
// Apply middleware to specific routes
e.GET("/admin/dashboard", adminDashboardHandler, isAdmin)
e.GET("/admin/users", listUsersHandler, isAdmin)
// Or to a group of routes
adminGroup := e.Group("/admin")
adminGroup.Use(isAdmin)
adminGroup.GET("/dashboard", adminDashboardHandler)
adminGroup.GET("/users", listUsersHandler)
Role-Based Access Control (RBAC)
RBAC is a common authorization approach where access decisions are based on the roles assigned to users. Here's how to implement a more flexible RBAC middleware:
// hasRole middleware checks if user has any of the required roles
func hasRole(roles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*models.User)
if user == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Unauthorized access",
})
}
// Check if user's role is in the permitted roles
for _, role := range roles {
if user.Role == role {
return next(c)
}
}
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
}
}
Usage example:
// Routes accessible by both admins and editors
e.GET("/articles", listArticlesHandler, hasRole("admin", "editor"))
// Routes accessible only by admins
e.DELETE("/articles/:id", deleteArticleHandler, hasRole("admin"))
Permission-Based Authorization
For more granular control, permission-based authorization assigns specific permissions to users instead of broad roles:
// hasPermission middleware checks if user has required permissions
func hasPermission(permission string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*models.User)
if user == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Unauthorized access",
})
}
// Check if user has the required permission
for _, perm := range user.Permissions {
if perm == permission {
return next(c)
}
}
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
}
}
Usage with permissions:
// Different routes with different permission requirements
e.POST("/articles", createArticleHandler, hasPermission("article:create"))
e.DELETE("/articles/:id", deleteArticleHandler, hasPermission("article:delete"))
e.PUT("/articles/:id", updateArticleHandler, hasPermission("article:update"))
JWT Claims for Authorization
If you're using JWT authentication, you can store authorization information in the token claims:
// JWTConfig setup with custom claims
jwtConfig := middleware.JWTConfig{
Claims: &CustomClaims{},
SigningKey: []byte("your-secret-key"),
}
// CustomClaims extends JWT standard claims
type CustomClaims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
jwt.StandardClaims
}
// Authorization middleware using JWT claims
func authorizeWithClaims(permission string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*CustomClaims)
// Check if user has required permission
for _, perm := range claims.Permissions {
if perm == permission {
return next(c)
}
}
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
}
}
Resource-Based Authorization
Sometimes authorization depends on the relationship between the user and the resource. For example, users should only edit their own posts:
// canModifyArticle checks if user is the author or an admin
func canModifyArticle(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*models.User)
articleID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid article ID",
})
}
// Get article from database
article, err := getArticleByID(articleID)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "Article not found",
})
}
// Check if user is author or admin
if article.AuthorID == user.ID || user.Role == "admin" {
// Add article to context so handler doesn't need to fetch it again
c.Set("article", article)
return next(c)
}
return c.JSON(http.StatusForbidden, map[string]string{
"error": "You don't have permission to modify this article",
})
}
}
Usage example:
e.PUT("/articles/:id", updateArticleHandler, canModifyArticle)
e.DELETE("/articles/:id", deleteArticleHandler, canModifyArticle)
Real-World Example: Blog Platform Authorization
Let's put everything together in a more comprehensive example of a blog platform with different user roles:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// User roles
const (
RoleAdmin = "admin"
RoleEditor = "editor"
RoleAuthor = "author"
RoleUser = "user"
)
// Permissions
const (
PermCreateArticle = "article:create"
PermEditArticle = "article:edit"
PermDeleteArticle = "article:delete"
PermManageUsers = "users:manage"
)
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Public routes
e.GET("/articles", listArticlesHandler)
e.GET("/articles/:id", getArticleHandler)
// Authentication middleware
// This is just a placeholder - you would use real authentication
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Mock user for demonstration
user := &User{
ID: 1,
Name: "John Doe",
Role: "editor",
Permissions: []string{
PermCreateArticle,
PermEditArticle,
},
}
c.Set("user", user)
return next(c)
}
})
// Protected routes
e.POST("/articles", createArticleHandler, hasPermission(PermCreateArticle))
e.PUT("/articles/:id", updateArticleHandler, hasPermission(PermEditArticle))
e.DELETE("/articles/:id", deleteArticleHandler, hasPermission(PermDeleteArticle))
// Admin routes
adminGroup := e.Group("/admin")
adminGroup.Use(hasRole(RoleAdmin))
adminGroup.GET("/users", listUsersHandler)
adminGroup.POST("/users", createUserHandler)
e.Logger.Fatal(e.Start(":8080"))
}
// hasRole middleware checks if user has specified role
func hasRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*User)
if !ok || user.Role != role {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
return next(c)
}
}
}
// hasPermission middleware checks if user has specific permission
func hasPermission(permission string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*User)
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Unauthorized access",
})
}
// Admins have all permissions
if user.Role == RoleAdmin {
return next(c)
}
// Check user permissions
for _, perm := range user.Permissions {
if perm == permission {
return next(c)
}
}
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
}
}
// Handler functions (implementations would be added in real code)
func listArticlesHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "List of articles"})
}
func getArticleHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Article details"})
}
func createArticleHandler(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{"message": "Article created"})
}
func updateArticleHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Article updated"})
}
func deleteArticleHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Article deleted"})
}
func listUsersHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "List of users"})
}
func createUserHandler(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{"message": "User created"})
}
Best Practices for Echo Authorization
-
Layer your authorization: Combine route-specific middleware with resource checks inside handlers.
-
Keep authorization logic separate: Create reusable middleware functions rather than repeating authorization checks.
-
Use descriptive error messages: In development, but be careful not to reveal sensitive details in production.
-
Fail secure: Default to denying access if unsure whether a user should have permission.
-
Audit authorization failures: Log failed authorization attempts to identify potential security issues.
-
Test your authorization: Write tests specifically to verify authorization works correctly.
-
Consider using authorization libraries: For complex scenarios, consider libraries like Casbin that provide advanced authorization models.
Common Pitfalls to Avoid
-
Forgetting to check authorization on all routes: Even if you have route groups, make sure all protected routes are covered.
-
Hardcoded authorization checks: Avoid hardcoding role checks directly in handlers.
-
Inconsistent permission naming: Establish a consistent naming convention for permissions.
-
Over-reliance on middleware: Some authorization decisions require contextual information only available inside handlers.
-
Not handling edge cases: Ensure your authorization code handles null users, missing permissions, etc.
Summary
Echo's flexibility allows you to implement a wide variety of authorization strategies to suit your application's needs. In this guide, we've covered:
- Basic role-based authorization middleware
- Permission-based authorization
- JWT claims for authorization
- Resource-based authorization controls
- A complete real-world example combining these approaches
By following these patterns, you can create secure, well-structured authorization systems for your Echo applications, ensuring users can only access the resources they're permitted to use.
Additional Resources and Exercises
Resources
Exercises
- Implement a multi-level RBAC system with role inheritance (e.g., Admin inherits Editor permissions).
- Create a middleware that checks both roles and resource ownership.
- Extend the JWT-based authorization to include time-limited permissions.
- Implement attribute-based access control (ABAC) where permissions depend on user attributes and resource properties.
- Create a permission caching system to reduce database lookups for authorization decisions.
Remember that authorization is a critical security component of your application. Always thoroughly test your authorization implementation and consider having it reviewed by security professionals for production applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)