Skip to main content

Echo Authorization Middleware

Authorization is a critical component of web application security that determines what actions users can perform after they've been authenticated. In this guide, we'll explore how to implement authorization middleware in the Echo framework to protect your Go web applications.

Introduction to Authorization Middleware

Authorization middleware acts as a security checkpoint between incoming requests and your application's handlers. Unlike authentication (which verifies who users are), authorization middleware determines what resources users can access based on their roles or permissions.

In Echo, middleware follows a functional approach where each middleware is a function that can be inserted into the request processing pipeline. Authorization middleware specifically examines the authenticated user's credentials to decide whether a request should proceed.

Basic Authorization Middleware

Let's start with a simple authorization middleware that checks whether a user has a required role:

go
package middleware

import (
"net/http"

"github.com/labstack/echo/v4"
)

// RequireRole middleware ensures the authenticated user has the specified role
func RequireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user from context (assuming it was set during authentication)
user := c.Get("user")

// If no user found, return unauthorized error
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

// Type assertion to access user properties
// This assumes user is a custom User struct with a HasRole method
if u, ok := user.(*User); ok {
if u.HasRole(role) {
// User has the required role, proceed to the next handler
return next(c)
}
}

// User doesn't have required role
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}
}
}

This middleware takes a role as a parameter and verifies that the authenticated user has that role before allowing the request to proceed.

Implementing a User Model with Role Support

For the above middleware to work, you'd need a User structure with role capabilities:

go
package middleware

// User represents an authenticated user
type User struct {
ID string
Username string
Roles []string
}

// HasRole checks if the user has the specified role
func (u *User) HasRole(role string) bool {
for _, r := range u.Roles {
if r == role {
return true
}
}
return false
}

Using the Authorization Middleware

Here's how you can apply the authorization middleware to protect specific routes:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/your-app/middleware" // Your custom middleware
)

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

// Add basic middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Public routes
e.GET("/", homeHandler)
e.GET("/about", aboutHandler)

// Admin routes - protected with role-based authorization
adminGroup := e.Group("/admin")
adminGroup.Use(middleware.RequireRole("admin"))
adminGroup.GET("/dashboard", adminDashboardHandler)
adminGroup.POST("/settings", updateSettingsHandler)

// User routes - protected with a different role
userGroup := e.Group("/user")
userGroup.Use(middleware.RequireRole("user"))
userGroup.GET("/profile", userProfileHandler)

e.Logger.Fatal(e.Start(":8080"))
}

func homeHandler(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the home page!")
}

func aboutHandler(c echo.Context) error {
return c.String(http.StatusOK, "About us page")
}

func adminDashboardHandler(c echo.Context) error {
return c.String(http.StatusOK, "Admin Dashboard - Authorized access only")
}

func updateSettingsHandler(c echo.Context) error {
return c.String(http.StatusOK, "Settings updated successfully")
}

func userProfileHandler(c echo.Context) error {
return c.String(http.StatusOK, "User profile page")
}

Advanced Permission-Based Authorization

For more granular control, you might want to implement permission-based authorization rather than just role-based:

go
package middleware

import (
"net/http"

"github.com/labstack/echo/v4"
)

// Permission represents a specific action a user can perform
type Permission string

const (
PermissionReadUsers Permission = "read:users"
PermissionCreateUsers Permission = "create:users"
PermissionUpdateUsers Permission = "update:users"
PermissionDeleteUsers Permission = "delete:users"
)

// User with permissions
type User struct {
ID string
Username string
Roles []string
Permissions []Permission
}

// HasPermission checks if the user has the specified permission
func (u *User) HasPermission(permission Permission) bool {
for _, p := range u.Permissions {
if p == permission {
return true
}
}
return false
}

// RequirePermission middleware ensures the user has the specified permission
func RequirePermission(permission Permission) 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, "Authentication required")
}

if u, ok := user.(*User); ok {
if u.HasPermission(permission) {
return next(c)
}
}

return echo.NewHTTPError(http.StatusForbidden, "You don't have permission to access this resource")
}
}
}

Practical Example: Multi-Tenant API with Authorization

Here's a more comprehensive example showing authorization in a multi-tenant API:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
custommw "github.com/your-app/middleware" // Your custom middleware
)

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

// Add basic middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("secret"),
ContextKey: "token",
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
Claims: &jwtCustomClaims{},
}))
e.Use(extractUserFromToken) // Custom middleware to extract user from JWT

// API routes
api := e.Group("/api")

// Users API
users := api.Group("/users")
users.GET("", listUsersHandler, custommw.RequirePermission(custommw.PermissionReadUsers))
users.POST("", createUserHandler, custommw.RequirePermission(custommw.PermissionCreateUsers))
users.PUT("/:id", updateUserHandler, custommw.RequirePermission(custommw.PermissionUpdateUsers))
users.DELETE("/:id", deleteUserHandler, custommw.RequirePermission(custommw.PermissionDeleteUsers))

// Organizations API - requires a specific role
orgs := api.Group("/organizations", custommw.RequireRole("org_admin"))
orgs.GET("", listOrganizationsHandler)
orgs.POST("", createOrganizationHandler)

e.Logger.Fatal(e.Start(":8080"))
}

// JWT custom claims
type jwtCustomClaims struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
jwt.StandardClaims
}

// Extract user from JWT token and add to context
func extractUserFromToken(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Get("token")
if token == nil {
return next(c)
}

// Extract claims
claims, ok := token.(*jwt.Token).Claims.(*jwtCustomClaims)
if !ok {
return next(c)
}

// Fetch user from database using claims.UserID
// This is just an example - you would typically query your database here
user := &custommw.User{
ID: claims.UserID,
Roles: claims.Roles,
// Set permissions based on user data from database
}

// Set user in context
c.Set("user", user)

return next(c)
}
}

// API handlers
func listUsersHandler(c echo.Context) error {
// Logic to list users
return c.JSON(http.StatusOK, map[string]string{"message": "Users listed successfully"})
}

func createUserHandler(c echo.Context) error {
// Logic to create user
return c.JSON(http.StatusCreated, map[string]string{"message": "User created successfully"})
}

func updateUserHandler(c echo.Context) error {
// Logic to update user
return c.JSON(http.StatusOK, map[string]string{"message": "User updated successfully"})
}

func deleteUserHandler(c echo.Context) error {
// Logic to delete user
return c.JSON(http.StatusOK, map[string]string{"message": "User deleted successfully"})
}

func listOrganizationsHandler(c echo.Context) error {
// Logic to list organizations
return c.JSON(http.StatusOK, map[string]string{"message": "Organizations listed successfully"})
}

func createOrganizationHandler(c echo.Context) error {
// Logic to create organization
return c.JSON(http.StatusCreated, map[string]string{"message": "Organization created successfully"})
}

Testing Authorization Middleware

Testing your authorization middleware is crucial to ensure it works as expected. Here's a basic test example:

go
package middleware

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

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

// Handler to test
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Success")
}

// Create a middleware chain with RequireRole middleware
middlewareFunc := RequireRole("admin")(handler)

// Test case 1: User has required role
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Create a test user with admin role
user := &User{
ID: "1",
Username: "admin",
Roles: []string{"admin", "user"},
}

// Set user in context
c.Set("user", user)

// Execute the middleware
err := middlewareFunc(c)

// Assert
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Success", rec.Body.String())

// Test case 2: User doesn't have required role
req = httptest.NewRequest(http.MethodGet, "/", nil)
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)

// Create a test user without admin role
user = &User{
ID: "2",
Username: "regular",
Roles: []string{"user"},
}

// Set user in context
c.Set("user", user)

// Execute the middleware
err = middlewareFunc(c)

// Assert that we got a forbidden error
httpError, ok := err.(*echo.HTTPError)
assert.True(t, ok)
assert.Equal(t, http.StatusForbidden, httpError.Code)
}

Best Practices for Authorization Middleware

When implementing authorization in Echo, consider these best practices:

  1. Separation of Concerns: Keep authentication and authorization separate, even though they're related.

  2. Granular Permissions: Use fine-grained permissions rather than just roles for more flexibility.

  3. Default to Deny: Make your application secure by default - require explicit permissions for access.

  4. Middleware Composition: Compose multiple middleware functions for complex authorization scenarios.

  5. Error Messages: Keep error messages generic to avoid exposing sensitive information.

  6. Audit Logging: Log authorization failures to detect potential security incidents.

  7. Context-Based Authorization: Consider request context (IP, time, resource) in addition to user roles.

  8. Regular Testing: Test your authorization middleware thoroughly with different scenarios.

Summary

Authorization middleware in Echo provides a powerful way to secure your application by controlling access to resources. By implementing role-based or permission-based authorization, you can ensure users can only access the resources they're entitled to.

We've covered:

  • Basic authorization middleware implementation
  • Role-based vs. permission-based approaches
  • Real-world examples in a multi-tenant API
  • Testing authorization middleware
  • Best practices for secure implementation

With these tools and patterns, you can build secure applications that enforce appropriate access controls throughout your Echo application.

Additional Resources

Exercises

  1. Implement a middleware that restricts access based on the user's subscription level (free, premium, enterprise).

  2. Create a middleware that checks if a user belongs to the same organization as the resource they're trying to access.

  3. Build a middleware that implements rate limiting based on the user's role (e.g., admin users get higher limits).

  4. Implement a resource-owner check middleware that ensures users can only modify resources they created.

  5. Design a middleware that combines both role-based and permission-based authorization.



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