Echo Authorization Context
Introduction
When building secure web applications using the Echo framework, one of the most important aspects to understand is the authorization context. Authorization context refers to the information and environment in which access control decisions are made in your application. It bridges the gap between authentication (verifying who a user is) and authorization (determining what they can do).
In this guide, we'll explore how Echo handles authorization context, how to set it up properly, and how to use it to make secure access control decisions in your applications.
Understanding Authorization Context
Authorization context in Echo typically contains:
- User information: Details about the authenticated user
- Roles and permissions: What actions the user is allowed to perform
- Request metadata: Information about the current request
- Environmental factors: Time, location, or other contextual information
This context is usually stored in the Echo Context (echo.Context
) after authentication and is accessed during authorization checks.
Setting Up Authorization Context
The first step in implementing authorization is to set up middleware that populates the context with necessary information after authentication.
Basic Authorization Context Middleware
Here's a simple example of middleware that sets authorization context:
func SetAuthContext(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the user from the token (assuming JWT authentication)
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
// Create auth context
authContext := map[string]interface{}{
"userID": claims["sub"],
"username": claims["name"],
"roles": claims["roles"],
"isAdmin": claims["roles"].([]interface{})[0] == "admin",
}
// Set auth context in Echo context
c.Set("auth", authContext)
return next(c)
}
}
Registering the Middleware
To use this middleware, register it in your Echo application:
func main() {
e := echo.New()
// JWT middleware
e.Use(middleware.JWT([]byte("your-secret-key")))
// Our authorization context middleware
e.Use(SetAuthContext)
// Protected routes
r := e.Group("/api")
r.GET("/profile", getProfile)
e.Logger.Fatal(e.Start(":1323"))
}
Accessing the Authorization Context
Once the authorization context is set, you can access it in your handlers:
func getProfile(c echo.Context) error {
// Get auth context
auth := c.Get("auth").(map[string]interface{})
// Access properties
userID := auth["userID"].(string)
return c.JSON(http.StatusOK, map[string]string{
"message": "Profile for user " + userID,
})
}
Role-Based Access Control with Auth Context
One common use of authorization context is implementing Role-Based Access Control (RBAC).
Creating a Role Check Middleware
func RequireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
auth := c.Get("auth").(map[string]interface{})
roles := auth["roles"].([]interface{})
// Check if user has the required role
hasRole := false
for _, r := range roles {
if r.(string) == role {
hasRole = true
break
}
}
if !hasRole {
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}
return next(c)
}
}
}
Using the Role Check Middleware
// Admin-only routes
admin := e.Group("/admin")
admin.Use(RequireRole("admin"))
admin.GET("/dashboard", adminDashboard)
Practical Example: Multi-Tenant Authorization
Let's consider a real-world scenario: a SaaS application with multiple organizations, where users belong to different organizations and have different roles within each.
Enhanced Authorization Context
func SetEnhancedAuthContext(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
// Get user ID from claims
userID := claims["sub"].(string)
// In a real app, you'd fetch this info from database
userOrgs, err := database.GetUserOrganizations(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Error fetching user data")
}
// Create rich auth context
authContext := map[string]interface{}{
"userID": userID,
"username": claims["name"],
"email": claims["email"],
"organizations": userOrgs,
"currentTime": time.Now().UTC(),
}
c.Set("auth", authContext)
return next(c)
}
}
Organization-Specific Authorization
func RequireOrgAccess(orgParam string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
auth := c.Get("auth").(map[string]interface{})
orgs := auth["organizations"].([]map[string]interface{})
// Get org ID from URL parameter
requestedOrgID := c.Param(orgParam)
// Check if user belongs to this organization
hasAccess := false
for _, org := range orgs {
if org["id"].(string) == requestedOrgID {
hasAccess = true
// Set current org context for convenience
c.Set("currentOrg", org)
break
}
}
if !hasAccess {
return echo.NewHTTPError(http.StatusForbidden, "No access to this organization")
}
return next(c)
}
}
}
Using Organization-Specific Middleware
// Organization routes
orgRoutes := e.Group("/orgs/:orgID")
orgRoutes.Use(RequireOrgAccess("orgID"))
// Now all these routes will check if the user has access to the specific org
orgRoutes.GET("/dashboard", orgDashboard)
orgRoutes.POST("/projects", createProject)
func orgDashboard(c echo.Context) error {
// The currentOrg is now available in the context
org := c.Get("currentOrg").(map[string]interface{})
return c.JSON(http.StatusOK, map[string]interface{}{
"organization": org["name"],
"dashboard": "Welcome to your organization dashboard",
})
}
Advanced: Permission-Based Authorization
For fine-grained control, you might need permission-based authorization:
func RequirePermission(permission string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
auth := c.Get("auth").(map[string]interface{})
// In a real app, you might calculate permissions based on roles
// Here we assume they're directly in the auth context
permissions := auth["permissions"].([]interface{})
hasPermission := false
for _, p := range permissions {
if p.(string) == permission {
hasPermission = true
break
}
}
if !hasPermission {
return echo.NewHTTPError(http.StatusForbidden, "Missing required permission: " + permission)
}
return next(c)
}
}
}
Best Practices for Authorization Context
- Least Privilege: Only include the minimum necessary information in the auth context
- Validation: Always validate the auth context data before using it
- Cache Wisely: For complex authorization rules, consider caching permission checks
- Audit Logging: Log important authorization decisions for security auditing
- Fail Securely: Default to denying access when authorization context is missing or invalid
Complete Example: Putting It All Together
Here's a more complete example showing how all pieces fit together:
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/golang-jwt/jwt/v4"
)
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Roles []string `json:"roles"`
}
type AuthContext struct {
User User `json:"user"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"requestId"`
Permissions []string `json:"permissions"`
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
// JWT middleware
jwtMiddleware := middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
})
// Public routes
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the API")
})
// Routes that require authentication
api := e.Group("/api")
api.Use(jwtMiddleware)
api.Use(SetAuthContext)
// Basic authenticated routes
api.GET("/me", getProfile)
// Admin routes
admin := api.Group("/admin")
admin.Use(RequireRole("admin"))
admin.GET("/users", listUsers)
// Specific permission routes
api.GET("/reports", generateReport, RequirePermission("reports:read"))
e.Logger.Fatal(e.Start(":1323"))
}
func SetAuthContext(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
// Build user object
u := User{
ID: claims["sub"].(string),
Username: claims["name"].(string),
Roles: parseRoles(claims["roles"]),
}
// Calculate permissions based on roles
permissions := calculatePermissions(u.Roles)
// Create auth context
authContext := AuthContext{
User: u,
Timestamp: time.Now(),
RequestID: c.Response().Header().Get(echo.HeaderXRequestID),
Permissions: permissions,
}
c.Set("auth", authContext)
return next(c)
}
}
func parseRoles(claimRoles interface{}) []string {
roles := []string{}
if rolesArr, ok := claimRoles.([]interface{}); ok {
for _, r := range rolesArr {
roles = append(roles, r.(string))
}
}
return roles
}
func calculatePermissions(roles []string) []string {
// In real app, this would be more sophisticated
permissions := []string{}
for _, role := range roles {
switch role {
case "admin":
permissions = append(permissions,
"users:read", "users:write",
"reports:read", "reports:write")
case "user":
permissions = append(permissions, "reports:read")
}
}
return permissions
}
func getProfile(c echo.Context) error {
auth := c.Get("auth").(AuthContext)
return c.JSON(http.StatusOK, map[string]interface{}{
"user": auth.User,
"requestInfo": map[string]interface{}{
"timestamp": auth.Timestamp,
"requestId": auth.RequestID,
},
})
}
func listUsers(c echo.Context) error {
// This would normally fetch from database
users := []User{
{ID: "1", Username: "john", Roles: []string{"admin"}},
{ID: "2", Username: "jane", Roles: []string{"user"}},
}
return c.JSON(http.StatusOK, users)
}
func generateReport(c echo.Context) error {
// This handler requires the "reports:read" permission
return c.JSON(http.StatusOK, map[string]string{
"report": "Your report data goes here",
})
}
Input/Output Examples
Example 1: Accessing Profile With Valid Auth Context
Request:
GET /api/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response:
{
"user": {
"id": "123",
"username": "john_doe",
"roles": ["admin"]
},
"requestInfo": {
"timestamp": "2023-07-15T14:22:15Z",
"requestId": "req-abc-123"
}
}
Example 2: Accessing Admin Resource Without Permission
Request:
GET /api/admin/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response: (For a user without admin role)
{
"message": "Insufficient permissions",
"error": "Forbidden",
"status": 403
}
Summary
Authorization context is a crucial concept in Echo applications for implementing secure access control. It provides a structured way to:
- Capture user identity and permissions after authentication
- Make context-aware authorization decisions
- Implement complex access control patterns like RBAC and PBAC
- Support multi-tenant applications
By properly setting up and using authorization context, you can ensure that your Echo applications have robust security while maintaining clean, readable code. Remember to follow best practices like principle of least privilege and failing securely to maximize the security of your implementation.
Additional Resources
Exercises
- Extend the authorization context to include time-based constraints (e.g., access only during business hours)
- Implement a caching layer for permission checks to improve performance
- Create a middleware that logs all authorization decisions for audit purposes
- Build a simple admin panel that shows the current authorization context for debugging
- Implement a more sophisticated permission system with hierarchical permissions (e.g., "reports:admin" implies "reports:read" and "reports:write")
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)