Skip to main content

Echo Role-based Access

Role-based access control (RBAC) is an essential component of modern web applications. It allows you to restrict access to certain parts of your application based on the roles assigned to authenticated users. In this tutorial, we'll learn how to implement role-based access control in Echo, a high-performance web framework for Go.

Introduction to Role-based Access Control

RBAC is a security mechanism that restricts system access to authorized users based on their roles within an organization. Instead of managing permissions for each individual user, permissions are grouped into roles, and users are assigned to these roles.

For example:

  • Admin role: Can access all parts of the application
  • Manager role: Can access reports and user management
  • User role: Can only access their own data and basic features

Echo doesn't have built-in RBAC functionality, but we can easily implement it using middleware and authentication.

Prerequisites

Before we begin, ensure you have:

  1. Basic understanding of Go programming language
  2. Echo framework installed (go get github.com/labstack/echo/v4)
  3. A working authentication system (JWT is recommended)

Implementing Role-based Access Control

Step 1: Define User Roles

First, let's define our user roles using constants:

go
package models

// User roles
const (
RoleAdmin = "admin"
RoleManager = "manager"
RoleUser = "user"
)

// User represents the user model
type User struct {
ID uint `json:"id" gorm:"primary_key"`
Username string `json:"username"`
Password string `json:"-"` // Password will not be exposed in JSON
Role string `json:"role"`
}

Step 2: Create a Role-based Middleware

Now, let's create a middleware that validates if a user has the required role:

go
package middleware

import (
"net/http"
"your-project/models"

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

// RoleRequired is a middleware that checks if a user has the required role
func RoleRequired(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user from context (set by your auth middleware)
user, ok := c.Get("user").(*models.User)

if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

// Check if user has the required role
if user.Role != role {
return echo.NewHTTPError(http.StatusForbidden, "You don't have permission to access this resource")
}

return next(c)
}
}
}

// HasAnyRole checks if a user has any of the provided roles
func HasAnyRole(roles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*models.User)

if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

for _, role := range roles {
if user.Role == role {
return next(c)
}
}

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

Step 3: Integrate with JWT Authentication

Let's integrate our role-based middleware with JWT authentication. First, we need to include the user's role in the JWT claims:

go
package handlers

import (
"net/http"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"your-project/models"
)

func Login(c echo.Context) error {
// Authenticate user (simplified for clarity)
username := c.FormValue("username")
password := c.FormValue("password")

// Validate user credentials from database
user := findUserByCredentials(username, password)

if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}

// Create token with claims
claims := jwt.MapClaims{
"id": user.ID,
"role": user.Role,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}

// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Generate encoded token
t, err := token.SignedString([]byte("your-secret-key"))
if err != nil {
return err
}

return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}

// Helper function to find a user (simplified)
func findUserByCredentials(username, password string) *models.User {
// In a real application, you would query the database
// and check password hash
// ...

return &models.User{
ID: 1,
Username: "admin",
Role: models.RoleAdmin,
}
}

Step 4: Extract User from JWT

Now we need to extract the user information from the JWT token in our auth middleware:

go
package middleware

import (
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"your-project/models"
)

// JWTConfig returns JWT middleware configuration
func JWTConfig() middleware.JWTConfig {
return middleware.JWTConfig{
Claims: &jwt.MapClaims{},
SigningKey: []byte("your-secret-key"),
}
}

// ExtractUserMiddleware extracts the user information from JWT claims
func ExtractUserMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// User will be available after JWT middleware
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(*jwt.MapClaims)

// Create a user from claims
user := &models.User{
ID: uint((*claims)["id"].(float64)),
Role: (*claims)["role"].(string),
}

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

return next(c)
}
}
}

Step 5: Applying Middleware to Routes

Let's put everything together by creating protected routes with role-based access:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"your-project/handlers"
customMiddleware "your-project/middleware"
"your-project/models"
)

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

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Public routes
e.POST("/login", handlers.Login)

// JWT middleware
jwtMiddleware := middleware.JWTWithConfig(customMiddleware.JWTConfig())

// Routes that require authentication
auth := e.Group("/api")
auth.Use(jwtMiddleware)
auth.Use(customMiddleware.ExtractUserMiddleware())

// User routes - accessible by all authenticated users
auth.GET("/profile", handlers.GetProfile)

// Manager routes - only for managers and admins
managers := auth.Group("/managers")
managers.Use(customMiddleware.HasAnyRole(models.RoleManager, models.RoleAdmin))
managers.GET("/reports", handlers.GetReports)

// Admin routes - only for admins
admin := auth.Group("/admin")
admin.Use(customMiddleware.RoleRequired(models.RoleAdmin))
admin.GET("/users", handlers.ListAllUsers)
admin.POST("/users", handlers.CreateUser)
admin.DELETE("/users/:id", handlers.DeleteUser)

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

Example Handler Functions

Let's implement some example handlers to demonstrate how our RBAC system works:

go
package handlers

import (
"net/http"

"github.com/labstack/echo/v4"
"your-project/models"
)

// GetProfile returns the authenticated user's profile
func GetProfile(c echo.Context) error {
user := c.Get("user").(*models.User)
return c.JSON(http.StatusOK, user)
}

// GetReports returns reports data (accessible by managers and admins)
func GetReports(c echo.Context) error {
reports := []map[string]interface{}{
{"id": 1, "name": "Sales Report", "data": "..."},
{"id": 2, "name": "User Activity", "data": "..."},
}
return c.JSON(http.StatusOK, reports)
}

// ListAllUsers returns all users (admin only)
func ListAllUsers(c echo.Context) error {
users := []models.User{
{ID: 1, Username: "admin", Role: models.RoleAdmin},
{ID: 2, Username: "manager", Role: models.RoleManager},
{ID: 3, Username: "user", Role: models.RoleUser},
}
return c.JSON(http.StatusOK, users)
}

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

// DeleteUser deletes a user (admin only)
func DeleteUser(c echo.Context) error {
id := c.Param("id")
// Logic to delete user with the given ID
return c.JSON(http.StatusOK, map[string]string{"message": "User " + id + " deleted successfully"})
}

Testing the Role-based Access Control

To test our implementation, let's try some API calls with different roles:

1. Login with Admin user

Request:

POST /login
Content-Type: application/x-www-form-urlencoded

username=admin&password=adminpassword

Response:

json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

2. Access Admin Route with Admin Token

Request:

GET /api/admin/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response (200 OK):

json
[
{"id": 1, "username": "admin", "role": "admin"},
{"id": 2, "username": "manager", "role": "manager"},
{"id": 3, "username": "user", "role": "user"}
]

3. Access Admin Route with User Token

Request:

GET /api/admin/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (user token)

Response (403 Forbidden):

json
{
"message": "You don't have permission to access this resource"
}

Real-world Application: Multi-tenant SaaS Platform

Let's consider a real-world example of how RBAC can be implemented in a multi-tenant SaaS application:

go
package models

// Enhanced role structure for multi-tenant app
const (
RoleSuperAdmin = "super_admin" // Can manage all tenants
RoleTenantAdmin = "tenant_admin" // Can manage a specific tenant
RoleTenantUser = "tenant_user" // Regular user within a tenant
)

type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
Role string `json:"role"`
TenantID uint `json:"tenant_id"` // Which tenant this user belongs to
}

type Tenant struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}

And a more sophisticated role checking middleware that checks both role and tenant:

go
// CheckTenantAccess ensures a user can only access their own tenant's data
func CheckTenantAccess(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*models.User)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

// Super admins can access any tenant
if user.Role == models.RoleSuperAdmin {
return next(c)
}

// Get tenant ID from request
tenantID, err := strconv.Atoi(c.Param("tenantId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid tenant ID")
}

// Check if user belongs to the requested tenant
if user.TenantID != uint(tenantID) {
return echo.NewHTTPError(http.StatusForbidden, "You don't have permission to access this tenant")
}

return next(c)
}
}

Summary

In this tutorial, we've learned how to implement role-based access control (RBAC) in Echo applications. We covered:

  1. Creating a custom middleware for role-based access control
  2. Integrating RBAC with JWT authentication
  3. Applying middleware to different route groups
  4. Testing the RBAC implementation with different user roles
  5. Implementing a real-world example for a multi-tenant SaaS application

Role-based access control is an essential security feature for any application with multiple user types. By implementing RBAC, you can ensure that users can only access the resources they're authorized to use, improving the security and organization of your application.

Additional Resources

Exercises

  1. Extend the RBAC system to include permission-based access control (more granular than roles).
  2. Implement a middleware to check if a user has access to a specific resource (e.g., can only edit their own posts).
  3. Create an admin panel that allows assigning roles to users.
  4. Implement an audit log system that tracks who accessed what resources.
  5. Add rate limiting for different user roles (e.g., regular users have stricter limits than admins).


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