middleware - Innovix-Matrix-Systems/ims-pocketbase-baas-starter GitHub Wiki
Custom Middleware Setup Guide
This guide explains how to set up and use custom authentication middleware in your PocketBase application.
Overview
The authentication middleware provides a clean interface for protecting routes using PocketBase's built-in apis.RequireAuth() functionality. It can be applied to both custom routes and default PocketBase API endpoints.
PocketBase Built-in Middleware APIs
PocketBase provides several built-in middleware helpers that you can use in your Go application:
apis.RequireAuth(...)- Requires authentication from any or specific auth collectionsapis.RequireGuestOnly()- Allows only unauthenticated requestsapis.RequireSuperuserAuth()- Requires superuser/admin authenticationapis.RequireSuperuserOrOwnerAuth(...)- Requires superuser or record owner authenticationapis.BodyLimit(...)- Limits request body sizeapis.Gzip()- Enables gzip compression- And more...
For complete documentation on PocketBase routing and middleware, see: Extend with Go - Routing - PocketBase Docs
Middleware Structure
The middleware system is located in internal/middlewares/ and follows a consistent pattern similar to routes and cron jobs:
middlewares.go- Main middleware registration following the same pattern as routes/cronsauth.go- Authentication middleware implementationmetrics.go- Metrics collection middleware implementationpermission.go- Permission-based access control middleware implementation
Middleware Registration Pattern
The middleware system uses a consistent array-based structure:
// Middleware represents an application middleware with its configuration
type Middleware struct {
ID string // Unique identifier for the middleware
Handler func(*core.RequestEvent) error // Handler function to execute
Enabled bool // Whether the middleware should be registered
Description string // Human-readable description of what the middleware does
Order int // Order of execution (lower numbers execute first)
}
// RegisterMiddlewares registers all application middlewares with the PocketBase router
func RegisterMiddlewares(e *core.ServeEvent) {
// Define all middlewares in a consistent array structure
middlewares := []Middleware{
{
ID: "metricsCollection",
Handler: getMetricsMiddlewareHandler(),
Enabled: true,
Description: "Collect HTTP request metrics",
Order: 1,
},
{
ID: "jwtAuth",
Handler: getAuthMiddlewareHandler(e),
Enabled: true,
Description: "JWT authentication with exclusions",
Order: 2,
},
}
// Register enabled middlewares
for _, middleware := range middlewares {
if !middleware.Enabled {
continue
}
e.Router.Bind(&hook.Handler[*core.RequestEvent]{
Id: middleware.ID,
Func: middleware.Handler,
})
}
}
Basic Usage
1. Initialize the Middleware
middleware := middlewares.NewAuthMiddleware()
2. Get Authentication Function
// Any authenticated user from any auth collection
authFunc := middleware.RequireAuthFunc()
// Only users from specific collections
authFunc := middleware.RequireAuthFunc("users")
authFunc := middleware.RequireAuthFunc("users", "admins")
Protecting Custom Routes
Method 1: Apply Middleware Inside Handler (Recommended)
func RegisterCustom(e *core.ServeEvent) {
middleware := middlewares.NewAuthMiddleware()
g := e.Router.Group("/api/v1")
// Public route (no auth required)
g.GET("/hello", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"msg": "Hello from custom route"})
})
// Protected route (auth required)
g.GET("/protected", func(e *core.RequestEvent) error {
// Apply authentication middleware
authFunc := middleware.RequireAuthFunc()
if err := authFunc(e); err != nil {
return err
}
// Your protected handler logic
return e.JSON(200, map[string]string{"msg": "You are authenticated!"})
})
}
Protecting Default PocketBase Routes
Method 1: Using OnRecordRequest Hooks (Recommended)
Apply authentication to all record operations:
func Run() {
app := pocketbase.New()
// Initialize middleware
middleware := middlewares.NewAuthMiddleware()
// Apply auth to all record operations
app.OnRecordRequest().BindFunc(func(e *core.RecordRequestEvent) error {
authFunc := middleware.RequireAuthFunc()
if err := authFunc(e.RequestEvent); err != nil {
return err
}
return e.Next()
})
// Rest of your app setup...
}
Method 2: Using OnServe Hook for Specific Routes
Apply authentication to specific PocketBase API endpoints:
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
middleware := middlewares.NewAuthMiddleware()
// Apply auth to specific PocketBase API endpoints
se.Router.Bind(&hook.Handler[*core.RequestEvent]{
Id: "customAuth",
Func: func(e *core.RequestEvent) error {
path := e.Request.URL.Path
if strings.HasPrefix(path, "/api/collections/") {
authFunc := middleware.RequireAuthFunc()
if err := authFunc(e); err != nil {
return err
}
}
return e.Next()
},
})
// Your existing routes...
return se.Next()
})
Method 3: Collection-Specific Hooks
Protect specific collection operations:
// Protect specific collection operations
app.OnRecordListRequest("users").BindFunc(func(e *core.RecordListRequestEvent) error {
authFunc := middleware.RequireAuthFunc()
if err := authFunc(e.RequestEvent); err != nil {
return err
}
return e.Next()
})
app.OnRecordViewRequest("users").BindFunc(func(e *core.RecordViewRequestEvent) error {
authFunc := middleware.RequireAuthFunc()
if err := authFunc(e.RequestEvent); err != nil {
return err
}
return e.Next()
})
Collection Filtering
You can restrict authentication to specific collections:
// Any auth collection (default)
authFunc := middleware.RequireAuthFunc()
// Only "users" collection
authFunc := middleware.RequireAuthFunc("users")
// Multiple collections
authFunc := middleware.RequireAuthFunc("users", "admins")
Error Handling
The middleware uses PocketBase's standard error handling:
- 401 Unauthorized: Returned when authentication fails
- Standard Format: Follows PocketBase's error response format
Example error response:
{
"code": 401,
"message": "The request requires valid record authorization token to be set.",
"data": {}
}
Testing Your Middleware
1. Test Public Routes
curl http://localhost:8090/api/v1/hello
# Should return: {"msg": "Hello from custom route"}
2. Test Protected Routes Without Auth
curl http://localhost:8090/api/v1/protected
# Should return: 401 Unauthorized
3. Test Protected Routes With Auth
# First, get an auth token
curl -X POST http://localhost:8090/api/collections/users/auth-with-password \
-H "Content-Type: application/json" \
-d '{"identity": "[email protected]", "password": "password"}'
# Use the token
curl http://localhost:8090/api/v1/protected \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Should return: {"msg": "You are authenticated!"}
Important Considerations
1. Avoid Breaking Auth Endpoints
Be careful not to apply authentication to PocketBase's auth endpoints:
/api/collections/users/auth-with-password/api/collections/users/auth-refresh/api/collections/users/request-password-reset
2. Collection Rules vs Middleware
Consider using PocketBase's built-in collection access rules for simpler use cases. Custom middleware is best for:
- Complex authentication logic
- Cross-collection validation
- Custom JWT validation (future enhancement)
- Logging and monitoring
3. Development vs Production
The middleware works the same in both environments, but consider:
- HTTPS enforcement in production
- Token storage security on the client side
- Rate limiting for auth endpoints
Permission Middleware
The permission middleware extends the authentication system to provide permission-based access control for custom routes. It checks if an authenticated user has specific permissions before allowing access to protected resources.
Overview
The permission middleware builds upon the existing RBAC (Role-Based Access Control) system where users can have:
- Direct permissions assigned to them
- Permissions inherited through roles
Basic Setup
The permission middleware is located in internal/middlewares/permission.go and provides:
PermissionMiddlewarestruct for organizing permission functionalityNewPermissionMiddleware()constructor functionRequirePermission()method that returns a middleware functionHasPermission()method for checking user permissions
Basic Usage with Single Permission
func RegisterCustomRoutes(e *core.ServeEvent) {
// Initialize middlewares
authMiddleware := middlewares.NewAuthMiddleware()
permMiddleware := middlewares.NewPermissionMiddleware()
g := e.Router.Group("/api/v1")
// Protected route requiring specific permission
g.GET("/admin/users", func(e *core.RequestEvent) error {
// First ensure user is authenticated
authFunc := authMiddleware.RequireAuthFunc()
if err := authFunc(e); err != nil {
return err
}
// Then check for specific permission
permFunc := permMiddleware.RequirePermission("users.view")
if err := permFunc(e); err != nil {
return err
}
// Handler logic for authorized users
return e.JSON(200, map[string]string{"message": "User list access granted"})
})
}
Usage with Multiple Permissions (ANY Logic)
// User needs ANY of these permissions to access the route
g.POST("/api/content", func(e *core.RequestEvent) error {
// Authentication first
authFunc := authMiddleware.RequireAuthFunc()
if err := authFunc(e); err != nil {
return err
}
// Permission check - user needs ANY of these permissions
permFunc := permMiddleware.RequirePermission("content.create", "content.admin", "content.manage")
if err := permFunc(e); err != nil {
return err
}
// Handler logic
return e.JSON(200, map[string]string{"message": "Content creation access granted"})
})
Integration with Route Groups
func RegisterAdminRoutes(e *core.ServeEvent) {
authMiddleware := middlewares.NewAuthMiddleware()
permMiddleware := middlewares.NewPermissionMiddleware()
// Admin route group
adminGroup := e.Router.Group("/api/admin")
// Apply authentication and admin permission to all routes in the group
adminGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Convert echo.Context to core.RequestEvent
e := c.(*core.RequestEvent)
// Apply authentication
authFunc := authMiddleware.RequireAuthFunc()
if err := authFunc(e); err != nil {
return err
}
// Apply admin permission check
permFunc := permMiddleware.RequirePermission("admin.access")
if err := permFunc(e); err != nil {
return err
}
return next(c)
}
})
// All routes in this group now require authentication and admin.access permission
adminGroup.GET("/dashboard", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"message": "Admin dashboard"})
})
adminGroup.GET("/settings", func(e *core.RequestEvent) error {
return e.JSON(200, map[string]string{"message": "Admin settings"})
})
}
Permission Error Handling
The permission middleware returns standard HTTP error responses:
- 403 Forbidden: Returned when user is authenticated but lacks required permissions
- 401 Unauthorized: Returned when user is not authenticated (handled by auth middleware)
Example error response for insufficient permissions:
{
"code": 403,
"message": "You don't have permission to access this resource",
"data": {}
}
Testing Permission Middleware
1. Test with User Having Required Permission
# First, authenticate and get a token for a user with the required permission
curl -X POST http://localhost:8090/api/collections/users/auth-with-password \
-H "Content-Type: application/json" \
-d '{"identity": "[email protected]", "password": "password"}'
# Use the token to access protected resource
curl http://localhost:8090/api/v1/admin/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Should return: {"message": "User list access granted"}
2. Test with User Lacking Required Permission
# Authenticate as a user without the required permission
curl -X POST http://localhost:8090/api/collections/users/auth-with-password \
-H "Content-Type: application/json" \
-d '{"identity": "[email protected]", "password": "password"}'
# Try to access protected resource
curl http://localhost:8090/api/v1/admin/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Should return: 403 Forbidden
Permission System Requirements
For the permission middleware to work, your PocketBase application should have:
- Users Collection: With authentication enabled
- Roles Collection: For role-based permissions
- Permissions Collection: Defining available permissions
- User-Role Relationships: Users can have multiple roles
- Role-Permission Relationships: Roles can have multiple permissions
- Direct User Permissions: Users can have direct permissions without roles
Best Practices
- Always Apply Authentication First: Permission middleware should be used after authentication middleware
- Use Descriptive Permission Names: Use clear, hierarchical permission names like
users.view,content.create - Group Related Routes: Apply permissions at the route group level when possible
- Test Permission Scenarios: Test with users having different permission combinations
- Admin Override: Admin users typically bypass permission checks
Future Extensions
The middleware is designed to be extensible. Future enhancements might include:
- Custom JWT validation logic
- Role-based authorization
- Token refresh handling
- Request logging and monitoring
- Rate limiting integration
Troubleshooting
Common Issues
-
"too many arguments in call to g.GET"
- Solution: Apply middleware inside the handler, not as a separate parameter
-
Auth not working on default routes
- Solution: Use OnRecordRequest hooks or OnServe hooks, not route-level middleware
-
Can't login after applying middleware
- Solution: Exclude auth endpoints from middleware protection
Debug Tips
- Add logging to see which routes are being protected
- Check the request path in middleware to ensure correct targeting
- Verify auth tokens are being sent correctly in requests
- Use PocketBase's admin UI to test authentication flows