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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)