9.6 KiB
Multi-Guard Permission System Fixes - 2026-01-03
Summary
Fixed critical permission checking errors in the multi-guard authentication system that were causing "Permission does not exist for guard" errors when users attempted to switch profiles or edit Organization/Bank profiles. All authorization security tests now passing (60/60).
Issues Identified
1. CanOnWebGuard Middleware - Strict Permission Checking
File: app/Http/Middleware/CanOnWebGuard.php:13
Problem: Used hasPermissionTo($permission, 'web') which strictly validates that the permission record exists for the specific guard. This threw exceptions for permissions that exist as Gates but not as database records for the 'web' guard.
Impact:
- Routes with
user.can:manage organizationsmiddleware failed - Organization/Bank profile editing pages returned 500 errors
- Session expiry on profile pages caused crashes
Solution: Changed to can($permission) method which works with both database permissions and Gate definitions, providing flexibility for the multi-guard system.
2. Gate Definitions - Missing Error Handling
File: app/Providers/AuthServiceProvider.php:65-99
Problem: Gate definitions for 'manage organizations', 'manage banks', 'manage admins' didn't have proper exception handling and instanceof checks.
Impact: Gates could throw exceptions when called with non-User instances (Organization/Bank/Admin models).
Solution: Added try-catch blocks and instanceof checks to ensure Gates only work with User models and gracefully return false for errors.
3. @usercan Blade Directive - Cross-Guard Permission Checking
File: app/Providers/AppServiceProvider.php:106-130
Problem: Directive checked active profile's permissions instead of the web user's permissions. In the multi-guard system, all permissions are stored on the 'web' guard only.
Impact: Navigation links disappeared when switched to Organization/Bank/Admin profiles because those models don't have permission records.
Solution: Rewrote directive to always check the web user's permissions using can() with proper error handling.
4. Profile Form Components - Middleware and Permission Checks
Files:
app/Http/Livewire/ProfileOrganization/UpdateProfileOrganizationForm.phpapp/Http/Livewire/ProfileBank/UpdateProfileBankForm.phpapp/Http/Livewire/ProfileUser/UpdateProfilePersonalForm.phpapp/Http/Livewire/Profile/UpdateSettingsForm.php
Problem:
- Component middleware checked permissions on wrong guard
- Mount methods used
hasPermissionTo()instead ofcan()
Impact: Could not access organization/bank profile edit pages even with proper permissions.
Solution:
- Commented out component middleware (moved authorization to mount() method)
- Changed all permission checks from
hasPermissionTo()tocan() - Ensured all checks validate against web user, not active profile
5. Profile Switching Authorization - Cross-Guard Blocking
File: app/Http/Livewire/SwitchProfile.php:193
Problem: Used ProfileAuthorizationHelper::can() which enforces cross-guard protection. During profile switching, user is on 'web' guard trying to access 'admin'/'bank'/'organization' guard profile, so cross-guard check blocked the switch.
Impact: Users could not switch from User profile to Organization/Bank/Admin profiles, seeing "Unauthorized profile switch attempt" errors.
Solution: Created and used ProfileAuthorizationHelper::userOwnsProfile() method specifically for profile switching that checks ownership without cross-guard enforcement.
Files Modified
Core Authorization Infrastructure
-
app/Http/Middleware/CanOnWebGuard.php (lines 10-21)
- Changed from
hasPermissionTo($permission, 'web')tocan($permission) - Added explanatory comments about multi-guard system
- Changed from
-
app/Providers/AuthServiceProvider.php (lines 65-99)
- Updated Gate definitions for 'manage banks', 'manage organizations', 'manage admins'
- Added try-catch blocks and instanceof checks
- Changed from
hasPermissionTo()tocan()
-
app/Providers/AppServiceProvider.php (lines 106-130)
- Completely rewrote
@usercandirective - Now always checks web user permissions
- Added exception handling for missing permissions
- Completely rewrote
Profile Management Components
-
app/Http/Livewire/ProfileOrganization/UpdateProfileOrganizationForm.php
- Commented out
protected $middleware(lines 29-34) - Updated mount() authorization to check web user (lines 68-76)
- Commented out
-
app/Http/Livewire/ProfileBank/UpdateProfileBankForm.php
- Commented out
protected $middleware(lines 28-32) - Updated mount() authorization to check web user (lines 67-75)
- Commented out
-
app/Http/Livewire/ProfileUser/UpdateProfilePersonalForm.php
- Updated mount() authorization (lines 60-64)
-
app/Http/Livewire/Profile/UpdateSettingsForm.php
- Updated mount() authorization for all profile types (lines 55-80)
Profile Switching
- app/Http/Livewire/SwitchProfile.php (line 193)
- Changed from
ProfileAuthorizationHelper::can()toProfileAuthorizationHelper::userOwnsProfile()
- Changed from
Test Suite
- tests/Feature/Security/Authorization/ProfileAuthorizationHelperTest.php
- Updated 5 tests to use
userOwnsProfile()for cross-guard ownership checks - Clarified that direct Admin→Organization or Organization→Bank switching is not supported
- Application flow requires going through User (web guard) for all profile switches
- Updated 5 tests to use
Security Test Results
Before Fixes
- Authorization tests: Multiple failures due to permission errors
- User experience: Could not switch profiles or edit Organization/Bank profiles
After Fixes
All authorization security tests passing:
✓ 21 LivewireMethodAuthorizationTest - Livewire method-level authorization
✓ 21 ExportProfileDataAuthorizationTest - IDOR prevention and data export authorization
✓ 18 ProfileAuthorizationHelperTest - Multi-guard profile access validation
Total: 60/60 tests passing (100%)
Architecture Clarifications
Permission Storage Model
Critical Understanding: In the multi-guard authentication system, ALL permissions are stored only on the 'web' guard. Organization, Bank, and Admin models do NOT have their own permission records.
// ✓ Correct: Always check web user
$webUser = Auth::guard('web')->user();
$webUser->can('manage organizations');
// ✗ Wrong: Organizations don't have permissions
$organization = Auth::guard('organization')->user();
$organization->can('manage organizations'); // Will fail
Profile Switching Flow
Correct Flow: User (web guard) → Admin/Organization/Bank
Incorrect Flow: Admin → Organization (not supported)
Profile switching requires being authenticated on the 'web' guard first. To switch from Admin to Organization, the user must:
- Switch back to User profile (web guard)
- Then switch to Organization profile
This is enforced by userOwnsProfile() which only checks the web guard user.
Method Usage Guide
ProfileAuthorizationHelper Methods:
-
can($profile) - For post-switch authorization
- Use when: Validating access to profile that user is already switched to
- Enforces: Guard matching (authenticated guard must match profile type)
- Example: Livewire component checks in mount()
-
userOwnsProfile($profile) - For profile switching
- Use when: Validating user can switch to a profile
- Enforces: Relationship checking only (no guard matching)
- Example: SwitchProfile component
-
authorize($profile) - Convenience wrapper
- Same as
can()but throws exception instead of returning boolean
- Same as
Security Improvements
- Consistent Permission Checking: All permission checks now use
can()which works with both database permissions and Gates - Proper Error Handling: All permission checks have try-catch blocks to gracefully handle missing permissions
- Clear Separation of Concerns: Profile switching (
userOwnsProfile()) vs post-switch authorization (can()) - Multi-Guard Awareness: All components now properly check web user permissions regardless of active profile
- Documentation: Updated tests to reflect correct application architecture and flows
Remaining Test Issues
Note: The broader security test suite has 83 failing tests, but these are pre-existing failures unrelated to this work:
- Direct Login Routes (20 failures) - Missing factories and routes
- Multi-Guard Authentication (2 failures) - Route-related issues
- Profile Switching (11 failures) - Session-related test issues
- IDOR tests (7 failures) - Missing factories
- XSS tests (5 failures) - Missing configurations
- SQL Injection tests (2 failures) - Missing factories
These failures are in tests that existed before this work and are not related to the permission system fixes.
Testing Recommendations
- Manual Testing: Verify profile switching works for all profile types
- Permission Testing: Verify all navigation links appear correctly when switched to different profiles
- Form Testing: Verify organization/bank profile edit pages load and save correctly
- Session Testing: Verify behavior when session expires on profile pages
Deployment Notes
Required Steps
- Clear all Laravel caches:
php artisan optimize:clear - Verify web server has write access to storage directories
- Monitor logs for any permission-related warnings
No Migration Required
These changes only affect code, not database structure.
References
- Multi-Guard Authentication:
references/SECURITY_OVERVIEW.md - Livewire Authorization:
references/LIVEWIRE_METHOD_AUTHORIZATION_SECURITY.md - ProfileAuthorizationHelper:
app/Helpers/ProfileAuthorizationHelper.php