Skip to content

[EPIC] Migrate Permission System from 'web' to 'sanctum' Guard #125

@kevalyq

Description

@kevalyq

🎯 Problem Statement

Current Architecture Mismatch:

  • Frontend: React PWA using Bearer tokens (stored in localStorage)
  • API Routes: All protected with auth:sanctum middleware
  • Permissions: Created with guard_name='web' (session-based)
  • User Model: No explicit $guard_name property (defaults to 'web')

Result: Spatie Laravel-Permission checks fail because:

  1. Sanctum authenticates user successfully ✅
  2. User attempts action requiring permission (e.g., roles.read)
  3. Spatie checks web guard for permissions ❌
  4. No permission found → 403 Forbidden

📊 Current State

Authentication Flow

Frontend Request
  ↓ Header: Authorization: Bearer <token>
API Middleware: auth:sanctum
  ↓ Token validated
User authenticated (sanctum guard)
  ↓ $user->can('roles.read')
Spatie checks 'web' guard ← MISMATCH!
  ❌ PermissionDoesNotExist: "There is no permission named `roles.read` for guard `sanctum`"

Affected Test Files

  • tests/Feature/RoleManagementApiTest.php (26 tests, 3 failing)
  • tests/Feature/RoleApiTest.php (27 tests, all failing with 403)
  • tests/Feature/PersonApiTest.php (13 tests, all failing with 403)

Total Impact: 36 failing tests due to guard mismatch

Code Locations

  1. Permission Creation (all test files):

    // Current (implicit web guard)
    Permission::create(['name' => 'roles.read']);
    
    // Target (explicit sanctum guard)
    Permission::create(['name' => 'roles.read', 'guard_name' => 'sanctum']);
  2. User Model (app/Models/User.php):

    // Current (no guard declared)
    class User extends Authenticatable { }
    
    // Target (explicit sanctum guard)
    class User extends Authenticatable {
        protected $guard_name = 'sanctum';
    }
  3. Auth Config (config/auth.php):

    // Already configured (✅ complete)
    'guards' => [
        'sanctum' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
    ],

🎯 Target State

Authentication Flow (After Migration)

Frontend Request
  ↓ Header: Authorization: Bearer <token>
API Middleware: auth:sanctum
  ↓ Token validated
User authenticated (sanctum guard)
  ↓ $user->can('roles.read')
Spatie checks 'sanctum' guard ← MATCH!
  ✅ Permission found → Authorization succeeds

Benefits

  1. Semantic Correctness: 'sanctum' guard explicitly documents token-based auth
  2. Zero Technical Debt: No confusion for future developers
  3. Self-Documenting: Code clearly states authentication mechanism
  4. Future-Proof: Correct foundation for expanding RBAC features
  5. Consistency: Guard name matches actual authentication method

📋 Sub-Issues & Implementation Tasks

✅ Acceptance Criteria

  • All permissions created with guard_name='sanctum' in tests
  • User model declares protected $guard_name = 'sanctum'
  • All 207 tests pass (including 36 previously failing)
  • PHPStan Level Max: 0 errors
  • Laravel Pint: Clean
  • Guard architecture documented
  • Config default guard set to 'sanctum'
  • No semantic confusion between guard names and auth mechanism

🔗 Related Issues

Resolved by:

Unblocks:

Context:

  • Created during RBAC Phase 4 implementation when guard mismatch was discovered
  • Architecture review determined systematic migration is preferred over quick fix

📚 Technical Context

Laravel Guards Explained

Guard: Authentication mechanism that determines HOW users are authenticated.

Web Guard:

  • Session/cookie-based authentication
  • Server stores session state
  • Cookie contains session ID
  • Typical for traditional web apps

Sanctum Guard:

  • Token-based authentication
  • Stateless (no server sessions)
  • Bearer token in Authorization header
  • Typical for SPAs and mobile apps

Spatie Permission Integration

Spatie Laravel-Permission is guard-aware - permissions are tied to specific guards. This allows:

  • Different permission sets per authentication mechanism
  • Fine-grained access control based on how user authenticated

Our Architecture: Pure token-based API (no sessions) → Should use 'sanctum' guard exclusively.

⏱️ Effort Estimate

  • Per Test File: 20-30 minutes (add guard_name to Permission::create calls) ✅ DONE
  • User Model: 5 minutes (add single line) ✅ DONE
  • Config Update: 10 minutes (change default guard) ✅ DONE
  • Documentation: 30-45 minutes ✅ DONE
  • Total: ~2-3 hours one-time investment ✅ COMPLETED

🚀 Migration Progress

Status: 🎉 100% COMPLETE - All sub-issues merged!

Risk: Low - isolated change, comprehensive test coverage


Created: 2025-11-09 during RBAC Phase 4 implementation
Completed: 2025-11-09 (same day!)
Status: ✅ All 6 sub-issues complete (100% done)
Priority: High (architectural correctness)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions