feat: replace token-based impersonation with enterprise-grade header-based system

Replaces the insecure token-in-localStorage approach with a header-based
impersonation system backed by cache sessions and MFA verification.

Key changes:
- New impersonation_sessions audit table (immutable, ULID PK)
- MFA verification required to start impersonation (TOTP/email/backup)
- X-Impersonate-User header + HandleImpersonation middleware
- Per-request auth context swap (admin session never modified)
- IP pinning, sensitive route blocking, no nesting, sliding 60-min TTL
- Activity log auto-tagged with impersonated_by during sessions
- Frontend: sessionStorage, BroadcastChannel sync, countdown timer
- ImpersonateDialog with reason + MFA verification flow
- 26 comprehensive tests covering core, middleware, audit, lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 02:42:53 +02:00
parent 47cb6b83d4
commit 4df668b5b8
25 changed files with 1813 additions and 269 deletions

View File

@@ -95,7 +95,7 @@ Route::post('portal/token-auth', [PortalTokenController::class, 'auth'])->middle
// Platform Admin routes
Route::prefix('admin')
->middleware(['auth:sanctum', 'role:super_admin'])
->middleware(['auth:sanctum', 'impersonation', 'role:super_admin'])
->name('admin.')
->group(function () {
// Organisations
@@ -115,12 +115,14 @@ Route::prefix('admin')
// Activity log
Route::get('activity-log', [AdminActivityLogController::class, 'index']);
// Impersonation (start)
// Impersonation — specific routes before wildcard
Route::get('impersonate/status', [AdminImpersonationController::class, 'status']);
Route::post('impersonate/send-mfa-code', [AdminImpersonationController::class, 'sendMfaCode']);
Route::post('impersonate/{user}', [AdminImpersonationController::class, 'start']);
});
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::middleware(['auth:sanctum', 'impersonation'])->group(function () {
// Impersonation (stop — accessible by impersonated user, not just super_admin)
Route::post('admin/stop-impersonation', [AdminImpersonationController::class, 'stop']);