By default, permission and role names are not included in exception messages to prevent information leakage.
config/permission.php
return [ /* * When set to true, the required permission names are added to exception messages. * This could be considered an information leak in some contexts, so the default * setting is false here for optimum safety. */ 'display_permission_in_exception' => false, /* * When set to true, the required role names are added to exception messages. * This could be considered an information leak in some contexts, so the default * setting is false here for optimum safety. */ 'display_role_in_exception' => false,];
Only enable display_permission_in_exception and display_role_in_exception in development or when you have proper error handling that logs these details without exposing them to end users.
Implement super-admin checks using Laravel’s Gate before callback to bypass permission checks safely.
app/Providers/AuthServiceProvider.php
use Illuminate\Support\Facades\Gate;public function boot(): void{ Gate::before(function ($user, $ability) { // Super admin bypasses all permission checks return $user->hasRole('super-admin') ? true : null; });}
Critical: Always return null (not false) when the super-admin check doesn’t apply. Returning false will deny access even if the user has the permission through other means.
Always specify the guard when using permission middleware for non-default authentication guards.
routes/web.php
Route::group(['middleware' => ['auth:admin', 'permission:admin-permission,admin']], function () { // Routes requiring admin guard});// Using static using() method for clarityRoute::get('/admin/users', [UserController::class, 'index']) ->middleware(PermissionMiddleware::using('manage-users', 'admin'));
Handling Multiple Guards in Middleware
When checking permissions, the middleware will use the guard of the authenticated user:
// User authenticated with 'admin' guard// Cannot access permission from 'web' guardAuth::guard('admin')->login($admin);$admin->givePermissionTo('admin-permission'); // guard_name: 'admin'// This worksRoute::middleware('permission:admin-permission,admin');// This fails - wrong guardRoute::middleware('permission:admin-permission,web'); // 403 Forbidden
For machine-to-machine authentication, configure Passport client credentials support carefully.
config/permission.php
return [ /* * Passport Client Credentials Grant * When set to true the package will use Passports Client to check permissions */ 'use_passport_client_credentials' => false,];
Permissions are preserved when users are soft-deleted, but detached on permanent deletion.
// Soft delete preserves permissions$user->delete(); // isForceDeleting() = false// User still has permissions$user = User::withTrashed()->find($userId);$user->hasPermissionTo('edit-articles'); // true// Force delete removes permissions$user->forceDelete(); // Permissions are detached
This behavior is implemented in the bootHasPermissions() method and automatically handles cleanup:
static::deleting(function ($model) { if (method_exists($model, 'isForceDeleting') && !$model->isForceDeleting()) { return; // Skip if soft deleting } $model->permissions()->detach(); // Only detach on force delete});
Customize column names for UUID or ULID primary keys to maintain security best practices.
config/permission.php
return [ 'column_names' => [ /* * Change this if you want to name the related model primary key other than * `model_id`. * * For example, this would be nice if your primary keys are all UUIDs. In * that case, name this `model_uuid`. */ 'model_morph_key' => 'model_id', 'role_pivot_key' => null, // default 'role_id' 'permission_pivot_key' => null, // default 'permission_id' ],];
UUIDs provide security benefits by preventing enumeration attacks:
Permission events are disabled by default. Enable only when you have proper monitoring in place.
config/permission.php
return [ /* * Events will fire when a role or permission is assigned/unassigned * To enable, set to true, and then create listeners to watch these events. */ 'events_enabled' => false,];
// When enabled, listen for security-relevant eventsuse Spatie\Permission\Events\PermissionAttachedEvent;use Spatie\Permission\Events\PermissionDetachedEvent;Event::listen(PermissionAttachedEvent::class, function ($event) { Log::info('Permission attached', [ 'user_id' => $event->model->id, 'permissions' => $event->permissionsOrIds, ]);});
Log permission changes for security auditing, but ensure logs are stored securely and comply with privacy regulations.