docs/advanced-guide/rbac/page.md
Role-Based Access Control (RBAC) is a security mechanism that restricts access to resources based on user roles and permissions. GoFr provides a pure config-based RBAC middleware that supports multiple authentication methods, fine-grained permissions, and role inheritance.
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Use default paths (configs/rbac.json, configs/rbac.yaml, configs/rbac.yml)
// Uses rbac.DefaultConfigPath internally (empty string triggers default path resolution)
// Tries configs/rbac.json, then configs/rbac.yaml, then configs/rbac.yml
app.EnableRBAC()
// Or with custom config path
app.EnableRBAC("configs/custom-rbac.json")
app.GET("/api/users", handler)
app.Run()
}
Configuration (configs/rbac.json):
{
"roleHeader": "X-User-Role",
"roles": [
{
"name": "admin",
"permissions": ["users:read", "users:write", "users:delete", "posts:read", "posts:write"]
},
{
"name": "editor",
"permissions": ["users:write", "posts:write"],
"inheritsFrom": ["viewer"]
},
{
"name": "viewer",
"permissions": ["users:read", "posts:read"]
}
],
"endpoints": [
{
"path": "/health",
"methods": ["GET"],
"public": true
},
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
},
{
"path": "/api/users",
"methods": ["POST"],
"requiredPermissions": ["users:write"]
}
]
}
💡 Best Practice: For production/public APIs, use JWT-based RBAC instead of header-based RBAC for better security.
Header-Based (for internal/trusted networks):
{
"roleHeader": "X-User-Role"
}
JWT-Based (for production/public APIs):
{
"jwtClaimPath": "role" // or "roles[0]", "permissions.role", etc.
}
Precedence: If both are set, only JWT is considered. The header is not checked when jwtClaimPath is configured, even if JWT extraction fails.
JWT Claim Path Formats:
"role" → {"role": "admin"}"roles[0]" → {"roles": ["admin", "user"]} (first element)"permissions.role" → {"permissions": {"role": "admin"}}{
"roles": [
{
"name": "admin",
"permissions": ["users:read", "users:write", "users:delete", "posts:read", "posts:write"] // Explicit permissions (wildcards not supported)
},
{
"name": "editor",
"permissions": ["users:write", "posts:write"], // Only additional permissions
"inheritsFrom": ["viewer"] // Inherits viewer's permissions
},
{
"name": "viewer",
"permissions": ["users:read", "posts:read"]
}
]
}
Note: When using inheritsFrom, only specify additional permissions - inherited ones are automatically included.
{
"endpoints": [
{
"path": "/health",
"methods": ["GET"],
"public": true // Bypasses authorization
},
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
},
{
"path": "/api/users/{id:[0-9]+}", // Mux pattern with constraint (numeric IDs only)
"methods": ["DELETE"],
"requiredPermissions": ["users:delete"]
},
{
"path": "/api/{resource}", // Single-level pattern - matches /api/users, /api/posts
"methods": ["GET"],
"requiredPermissions": ["api:read"]
},
{
"path": "/api/{path:.*}", // Multi-level pattern - matches /api/users/123, /api/posts/comments
"methods": ["*"], // All methods
"requiredPermissions": ["admin:read", "admin:write"] // Multiple permissions (OR logic)
},
{
"path": "/api/{category}/posts", // Middle variable - matches /api/tech/posts, /api/news/posts
"methods": ["GET"],
"requiredPermissions": ["posts:read"]
}
]
}
RBAC uses gorilla/mux route pattern conventions for endpoint matching. This ensures perfect alignment with how routes are registered in GoFr.
Important: The RBAC middleware uses the same router configuration as GoFr's application router (StrictSlash(false)), ensuring consistent behavior for trailing slashes. This means /api/users and /api/users/ are treated as the same route in both RBAC authorization checks and actual route matching.
Pattern Types:
"/api/users" matches exactly /api/users"/api/users/{id}" matches /api/users/123, /api/users/abc (any single segment)"/api/users/{id:[0-9]+}" matches /api/users/123 (numeric IDs only)"/api/{resource}" matches /api/users, /api/posts (one segment)"/api/{path:.*}" matches /api/users/123, /api/posts/comments (any depth)"/api/{category}/posts" matches /api/tech/posts, /api/news/postsCommon Patterns:
"/api/users/{id:[0-9]+}" (matches /api/users/123)"/api/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}" (matches /api/users/550e8400-e29b-41d4-a716-446655440000)"/api/users/{name:[a-zA-Z0-9]+}" (matches /api/users/user123)Grouped Endpoints:
For endpoints that need to match multiple paths, use mux patterns:
Single-level wildcard: Use "/api/{resource}" instead of "/api/*"
/api/users, /api/posts (one segment)Multi-level wildcard: Use "/api/{path:.*}" instead of "/api/*"
/api/users/123, /api/posts/comments (any depth)Middle variable: Use "/api/{category}/posts" instead of "/api/*/posts"
/api/tech/posts, /api/news/postsFor production/public APIs, use JWT-based role extraction:
app := gofr.New()
// Enable OAuth middleware first (required for JWT validation)
app.EnableOAuth("https://auth.example.com/.well-known/jwks.json", 10)
// Enable RBAC with config path (or use app.EnableRBAC() for default paths using rbac.DefaultConfigPath)
app.EnableRBAC("configs/rbac.json")
Configuration (configs/rbac.json):
{
"jwtClaimPath": "role", // or "roles[0]", "permissions.role", etc.
"roles": [...],
"endpoints": [...]
}
For business logic, you can access the user's role from the request context:
JWT-Based RBAC (when using JWT role extraction):
import (
"encoding/json"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http"
)
// JWTClaims represents the JWT claims structure
type JWTClaims struct {
Role string `json:"role"`
Sub string `json:"sub"`
// Add other claim fields as needed
}
func handler(ctx *gofr.Context) (interface{}, error) {
// Get JWT claims from context
claimsMap := ctx.GetAuthInfo().GetClaims()
if claimsMap == nil {
return nil, http.ErrorInvalidParam{Params: []string{"authorization"}}
}
// Convert map claims to struct (recommended GoFr pattern)
var claims JWTClaims
claimsBytes, err := json.Marshal(claimsMap)
if err != nil {
return nil, http.ErrorInvalidParam{Params: []string{"claims"}}
}
if err := json.Unmarshal(claimsBytes, &claims); err != nil {
return nil, http.ErrorInvalidParam{Params: []string{"claims"}}
}
// Use role for business logic (e.g., personalize UI, filter data)
// The role field matches the jwtClaimPath configured in rbac.json
return map[string]string{"userRole": claims.Role}, nil
}
Note: All authorization is handled automatically by the middleware. Accessing the role in handlers is only for business logic purposes (e.g., personalizing UI, filtering data).
Use the format: resource:action
users, posts, orders)read, write, delete, update)"users:read" // Read users
"users:write" // Create/update users
"users:delete" // Delete users
"posts:read" // Read posts
"posts:write" // Create/update posts
"orders:approve" // Approve orders
"reports:export" // Export reports
Avoid inconsistent formats:
"read_users", "writeUsers", "DELETE_POSTS""users:read", "users:write", "posts:delete"Important: Wildcards are NOT supported in permissions. Only exact matches are allowed.
"*:*" - Does not match all permissions"users:*" - Does not match all user permissions"users:read" - Exact match only"users:write" - Exact match onlyIf you need multiple permissions, specify them explicitly:
{
"name": "admin",
"permissions": ["users:read", "users:write", "users:delete", "posts:read", "posts:write"]
}
Or use role inheritance to avoid duplication:
{
"name": "editor",
"permissions": ["users:write", "posts:write"],
"inheritsFrom": ["viewer"] // Inherits viewer's permissions
}
{
"roles": [
{
"name": "admin",
"permissions": ["users:delete"],
"inheritsFrom": ["editor"]
},
{
"name": "editor",
"permissions": ["users:create", "users:update"],
"inheritsFrom": ["viewer"]
},
{
"name": "viewer",
"permissions": ["users:read"]
}
],
"endpoints": [
{
"path": "/api/users",
"methods": ["POST"],
"requiredPermissions": ["users:create"]
},
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
},
{
"path": "/api/users/{id:[0-9]+}",
"methods": ["PUT", "PATCH"],
"requiredPermissions": ["users:update"]
},
{
"path": "/api/users/{id:[0-9]+}",
"methods": ["DELETE"],
"requiredPermissions": ["users:delete"]
}
]
}
{
"roles": [
{
"name": "admin",
"permissions": ["own:posts:read", "own:posts:write", "all:posts:read", "all:posts:write"]
},
{
"name": "author",
"permissions": ["own:posts:read", "own:posts:write"]
},
{
"name": "viewer",
"permissions": ["own:posts:read", "all:posts:read"]
}
],
"endpoints": [
{
"path": "/api/posts/my-posts",
"methods": ["GET"],
"requiredPermissions": ["own:posts:read"]
},
{
"path": "/api/posts",
"methods": ["GET"],
"requiredPermissions": ["all:posts:read"]
}
]
}
resource:action format (e.g., users:read, posts:write)Role not being extracted
roleHeader or jwtClaimPath is set in config filePermission checks failing
roles[].permissions is properly configuredendpoints[].requiredPermissions matches your routes correctlyPermission always denied
roles[].permissions includes the required permissionPermission always allowed
public: trueendpoints[].requiredPermissions is set correctlyJWT role extraction failing
Config file not found
configs/rbac.json, configs/rbac.yaml, configs/rbac.yml)Route not being protected by RBAC
endpoints[] array["*"] for all methods)X-User-Role) or JWT claimsThe middleware automatically handles all authorization - you just define routes normally.
Important: RBAC only enforces authorization for endpoints that are explicitly configured in the RBAC config file.
Example:
{
"endpoints": [
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
}
]
}
In this configuration:
GET /api/users → RBAC enforced (requires users:read permission)POST /api/users → Not in RBAC config → Allowed to proceed (may return 404 if route doesn't exist)GET /api/posts → Not in RBAC config → Allowed to proceed (may return 404 if route doesn't exist)GET /health → Not in RBAC config → Allowed to proceed (will work if route exists)This design allows you to:
RBAC middleware implements industry-standard security practices to protect sensitive data:
Traces (OpenTelemetry):
Metrics:
Logs:
RBAC middleware never logs: