Overview
The SpatieAuthorize middleware provides centralized, convention-based authorization by automatically deriving required permissions from route metadata. It integrates with Spatie Laravel Permission and uses the in-memory permission cache for zero-database-hit authorization checks.
This middleware uses Spatie’s PermissionRegistrar for cache-aware permission lookups, ensuring high performance even with complex permission structures.
How It Works
The middleware derives required permissions from (in priority order):
- Route action overrides - Explicit permission definitions via
->defaults('authorize', [...])
- Route name conventions - Maps route names like
articles.store to permissions
- Controller@method conventions - Derives from
ArticleController@store
- HTTP verb + URI conventions - Last resort fallback
Constructor
registrar
PermissionRegistrar
required
Spatie’s permission registrar (automatically injected by Laravel)
Middleware Parameters
guard
string
default:"config('auth.defaults.guard')"
Authentication guard to use (e.g., api, web)
Route Registration
Basic Usage
use Illuminate\Support\Facades\Route;
Route::middleware('auto.authorize')
->group(function () {
Route::get('/articles', [ArticleController::class, 'index']);
Route::post('/articles', [ArticleController::class, 'store']);
});
Custom Guard
Route::middleware('auto.authorize:api')
->prefix('api')
->group(function () {
Route::resource('users', UserController::class);
});
Permission Resolution
Route Name Convention
The middleware maps standard resource route names to permissions:
| Route Name | Derived Permission |
|---|
articles.index | articles.index |
articles.show | articles.view |
articles.store | articles.store |
articles.update | articles.update |
articles.destroy | articles.delete |
// Route definition
Route::get('/articles', [ArticleController::class, 'index'])
->name('articles.index');
// Requires permission: articles.index
Controller Method Convention
Derives permissions from controller and method names:
// ArticleController@store
// Derived permission: article.create
// UserController@destroy
// Derived permission: user.delete
Mapping:
| Method | Permission Suffix |
|---|
index | view |
show | view |
store | create |
update | update |
destroy | delete |
HTTP Verb Convention
Last resort: maps HTTP verb + first URI segment:
| Verb | URI | Permission |
|---|
| GET | /articles | article.view |
| POST | /articles | article.create |
| PUT | /articles/1 | article.update |
| PATCH | /articles/1 | article.update |
| DELETE | /articles/1 | article.delete |
Explicit Route Overrides
Override automatic resolution with explicit permission requirements:
Single Permission
Route::post('/articles', [ArticleController::class, 'store'])
->name('articles.store')
->defaults('authorize', [
'permissions' => 'articles.create',
]);
Multiple Permissions (OR)
Route::put('/articles/{article}/publish', [ArticleController::class, 'publish'])
->defaults('authorize', [
'permissions' => ['articles.publish', 'articles.manage'],
'mode' => 'any', // User needs at least ONE permission (default)
]);
Multiple Permissions (AND)
Route::delete('/users/{user}', [UserController::class, 'destroy'])
->defaults('authorize', [
'permissions' => ['users.delete', 'users.manage'],
'mode' => 'all', // User needs ALL permissions
]);
Pipe-Separated Syntax
Route::post('/reports/generate', [ReportController::class, 'generate'])
->defaults('authorize', [
'permissions' => 'reports.view|reports.export',
'mode' => 'any',
]);
Custom Guard Override
Route::post('/admin/settings', [AdminController::class, 'update'])
->defaults('authorize', [
'permissions' => 'admin.settings.update',
'guard' => 'admin', // Override middleware guard
]);
Guard Selection Priority
- Route override:
->defaults('authorize', ['guard' => 'api'])
- Middleware parameter:
auto.authorize:api
- Application default:
config('auth.defaults.guard')
Configuration Overrides
Create config/permission_map.php for additional customization:
return [
// Strict mode: abort if permission doesn't exist in cache
'strict' => env('PERMISSION_STRICT_MODE', false),
// Custom route name mappings
'overrides' => [
'api.articles.publish' => 'articles.publish',
'api.users.suspend' => 'users.manage',
],
];
Teams and Tenants
For multi-tenant applications using Spatie’s team support:
namespace App\Http\Middleware;
use Spatie\Permission\PermissionRegistrar;
class SetPermissionTeam
{
public function handle($request, \Closure $next)
{
// Set the current team/tenant for permission checks
$teamId = $request->user()?->current_team_id;
if ($teamId) {
app(PermissionRegistrar::class)
->setPermissionsTeamId($teamId);
}
return $next($request);
}
}
Register before auto.authorize:
Route::middleware(['auth:api', 'set-team', 'auto.authorize'])
->group(function () {
// Routes
});
Error Responses
Unauthorized Access
{
"message": "Forbidden"
}
Debug Mode (app.debug = true)
{
"message": "Forbidden: articles.create"
}
Usage Examples
RESTful Resource
Route::middleware('auto.authorize')
->resource('articles', ArticleController::class);
// Automatic permissions:
// GET /articles → articles.index
// GET /articles/{id} → articles.view
// POST /articles → articles.store
// PUT /articles/{id} → articles.update
// DELETE /articles/{id} → articles.delete
Mixed Automatic and Explicit
Route::middleware('auto.authorize')->group(function () {
// Automatic: articles.index
Route::get('/articles', [ArticleController::class, 'index'])
->name('articles.index');
// Explicit override
Route::post('/articles/{id}/publish', [ArticleController::class, 'publish'])
->defaults('authorize', [
'permissions' => ['articles.publish', 'articles.manage'],
'mode' => 'any',
]);
});
API with Custom Guard
Route::prefix('api/v1')
->middleware(['auth:api', 'auto.authorize:api'])
->group(function () {
Route::apiResource('users', UserController::class);
Route::apiResource('posts', PostController::class);
});
The middleware uses Spatie’s in-memory permission cache (PermissionRegistrar), resulting in zero database queries for permission checks after initial load.
Strict Mode
Enable strict mode to catch permission typos in development:
// config/permission_map.php
return [
'strict' => env('PERMISSION_STRICT_MODE', true),
];
When enabled, the middleware will abort if a required permission doesn’t exist in the cache.
See Also