Skip to main content

Echo Scope Management

Authorization in web applications often requires fine-grained control over what resources users can access and what actions they can perform. Echo's scope management provides a powerful way to define and check these permissions in your applications.

Understanding Scopes

Scopes represent permissions that define what actions can be performed on what resources. They are typically strings that follow patterns like:

  • resource:action (e.g., users:read)
  • resource:action:modifier (e.g., documents:edit:own)

Scope-based authorization offers more granular control than simple role-based systems, allowing you to precisely define what each user can do.

Setting Up Scope Management in Echo

Let's start by implementing basic scope management in an Echo application:

go
package main

import (
"net/http"

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

// User represents a user with scopes
type User struct {
ID int
Name string
Scopes []string
}

// ScopeContext is custom context with scope checking capabilities
type ScopeContext struct {
echo.Context
User *User
}

// HasScope checks if user has the required scope
func (c *ScopeContext) HasScope(scope string) bool {
if c.User == nil {
return false
}

for _, s := range c.User.Scopes {
if s == scope {
return true
}
}
return false
}

// RequireScope middleware to check if user has required scope
func RequireScope(scope string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sc, ok := c.(*ScopeContext)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid context")
}

if !sc.HasScope(scope) {
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}

return next(c)
}
}
}

// ScopeMiddleware creates a scope context for each request
func ScopeMiddleware(getUserFunc func(c echo.Context) *User) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := getUserFunc(c)
sc := &ScopeContext{
Context: c,
User: user,
}
return next(sc)
}
}
}

Using Scopes in Routes

Now let's see how to apply these scopes to protect your API routes:

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

// Sample function to get user from request
getUserFunc := func(c echo.Context) *User {
// In a real application, you would extract user from
// JWT token, session, or another authentication method
return &User{
ID: 1,
Name: "John Doe",
Scopes: []string{"users:read", "documents:write:own"},
}
}

// Register middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(ScopeMiddleware(getUserFunc))

// Public routes
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the API")
})

// Protected routes with scope requirements
e.GET("/users", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "User list retrieved"})
}, RequireScope("users:read"))

e.POST("/documents", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Document created"})
}, RequireScope("documents:write:own"))

e.PUT("/admin/settings", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Settings updated"})
}, RequireScope("admin:settings:write"))

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

Testing the Scope Management

Let's test our API with different scopes to see how authorization works:

  1. The route /users will be accessible because our test user has the users:read scope
  2. The route /documents will be accessible with the documents:write:own scope
  3. The route /admin/settings will return a 403 Forbidden error because our user lacks the admin:settings:write scope

Example output when accessing /admin/settings:

json
{
"message": "insufficient permissions"
}

Advanced Scope Patterns

Hierarchical Scopes

You can implement hierarchical scopes to simplify permission management:

go
// HasScope with hierarchical scope checking
func (c *ScopeContext) HasScope(scope string) bool {
if c.User == nil {
return false
}

// Check for exact scope match
for _, s := range c.User.Scopes {
if s == scope {
return true
}

// Check for wildcard scopes (admin:*)
if s == "*" || s == scope+".*" || (s[len(s)-1] == '*' && strings.HasPrefix(scope, s[:len(s)-1])) {
return true
}
}
return false
}

Scope Combinations

You can also check for combinations of scopes:

go
// RequireAnyScope checks if user has any of the required scopes
func RequireAnyScope(scopes ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sc, ok := c.(*ScopeContext)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid context")
}

for _, scope := range scopes {
if sc.HasScope(scope) {
return next(c)
}
}

return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}
}
}

// RequireAllScopes checks if user has all the required scopes
func RequireAllScopes(scopes ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sc, ok := c.(*ScopeContext)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid context")
}

for _, scope := range scopes {
if !sc.HasScope(scope) {
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}
}

return next(c)
}
}
}

Practical Example: API for a Document Management System

Let's see how to implement scope management in a more realistic application:

go
func setupRoutes(e *echo.Echo) {
// Document management API
docs := e.Group("/documents")

// List documents (requires read permission)
docs.GET("", listDocuments, RequireScope("documents:read"))

// View a document (requires read permission)
docs.GET("/:id", getDocument, RequireScope("documents:read"))

// Create a document (requires write permission)
docs.POST("", createDocument, RequireScope("documents:write"))

// Update a document (requires both read and write permissions)
docs.PUT("/:id", updateDocument, RequireAllScopes("documents:read", "documents:write"))

// Delete a document (requires delete permission)
docs.DELETE("/:id", deleteDocument, RequireScope("documents:delete"))

// Share a document (requires share permission)
docs.POST("/:id/share", shareDocument, RequireScope("documents:share"))

// Admin operations (requires admin permission)
admin := e.Group("/admin")
admin.Use(RequireScope("admin"))

admin.GET("/documents", adminListAllDocuments)
admin.DELETE("/documents/:id/force", adminForceDeleteDocument)
}

// Handler examples
func listDocuments(c echo.Context) error {
sc := c.(*ScopeContext)
// Implementation would check if user can only see own documents
// or all documents based on their specific scopes
if sc.HasScope("documents:read:all") {
// Return all documents
} else if sc.HasScope("documents:read:own") {
// Return only user's documents
}
return c.JSON(http.StatusOK, map[string]string{"message": "Documents listed"})
}

func createDocument(c echo.Context) error {
// Create document logic here
return c.JSON(http.StatusCreated, map[string]string{"message": "Document created"})
}

Integration with JWT Authentication

It's common to store scopes in JWT tokens. Here's how to integrate scope management with JWT:

go
func jwtMiddleware() echo.MiddlewareFunc {
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("your-secret-key"),
}
return middleware.JWTWithConfig(config)
}

type jwtCustomClaims struct {
Name string `json:"name"`
UserID int `json:"userId"`
Scopes []string `json:"scopes"`
jwt.StandardClaims
}

func getScopesFromJWT(c echo.Context) *User {
user := &User{}
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(*jwtCustomClaims)

user.ID = claims.UserID
user.Name = claims.Name
user.Scopes = claims.Scopes

return user
}

// In main function
func main() {
e := echo.New()

// Apply JWT middleware before scope middleware
e.Use(jwtMiddleware())
e.Use(ScopeMiddleware(getScopesFromJWT))

// Setup routes
setupRoutes(e)

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

Best Practices for Scope Management

  1. Consistent Naming: Use consistent patterns for scope names, like resource:action or resource:action:modifier.

  2. Granular Scopes: Define scopes at an appropriate level of granularity. Too broad scopes reduce security, while too fine-grained scopes increase complexity.

  3. Documentation: Document all available scopes and their purposes for both developers and API users.

  4. Default Deny: Always default to denying access unless a scope explicitly grants permission.

  5. Scope Validation: Validate scopes during token issuance to ensure only valid scopes are assigned.

  6. Audit Trail: Log scope checks for security auditing purposes.

go
func RequireScopeWithAudit(scope string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sc, ok := c.(*ScopeContext)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid context")
}

hasScope := sc.HasScope(scope)

// Audit the scope check
log.Printf("Scope check: %s, User: %d, Result: %v",
scope, sc.User.ID, hasScope)

if !hasScope {
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}

return next(c)
}
}
}

Summary

Effective scope management is crucial for building secure APIs and web applications with Echo. In this guide, we've covered:

  • The concept of scopes and their structure
  • How to implement basic scope checking middleware
  • Advanced scope patterns including hierarchical scopes and scope combinations
  • Integration with JWT authentication
  • Best practices for scope management

By implementing proper scope management, you can create secure, fine-grained access control for your applications that can scale with complex requirements.

Additional Resources and Exercises

Resources:

Exercises:

  1. Basic Scope Implementation: Implement the basic scope management system shown in this guide in a new Echo application.

  2. Hierarchical Scopes: Extend the scope management to support hierarchical scopes (e.g., admin:* grants all admin permissions).

  3. Scope Visualization Tool: Create a simple web page that shows all available scopes in your system and their relationships.

  4. Resource-Based Scopes: Implement a system where scopes can be applied to specific resources (e.g., documents:read:123 allows reading only document with ID 123).

  5. Scope Auditing: Add a logging system that records all scope checks and their results for security auditing.



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