hasRole('super_admin')) { abort(403, 'Only super admins can impersonate users.'); } if (! $admin->mfa_enabled) { abort(403, 'MFA must be enabled to impersonate users.'); } if ($targetUser->hasRole('super_admin')) { abort(403, 'Cannot impersonate another super admin.'); } // Cannot nest: admin already impersonating someone if ($this->getActiveSessionForAdmin($admin)) { abort(403, 'You already have an active impersonation session.'); } // Cannot impersonate a user already being impersonated if ($this->getActiveSessionForTarget($targetUser)) { abort(403, 'This user is already being impersonated.'); } // ─── MFA Verification ─── $this->verifyMfaCode($admin, $mfaCode, $mfaMethod); // ─── Create session ─── $session = ImpersonationSession::create([ 'admin_id' => $admin->id, 'target_user_id' => $targetUser->id, 'reason' => $reason, 'mfa_method' => $mfaMethod->value, 'ip_address' => $ipAddress, 'user_agent' => $userAgent, 'started_at' => now(), 'expires_at' => now()->addSeconds(self::SESSION_TTL_SECONDS), ]); // Store in Redis for fast middleware lookups $this->storeCacheSession($session); // Activity log activity('admin') ->causedBy($admin) ->performedOn($targetUser) ->event('admin.impersonation.started') ->withProperties([ 'session_id' => $session->id, 'admin_id' => $admin->id, 'target_user_id' => $targetUser->id, 'reason' => $reason, 'mfa_method' => $mfaMethod->value, ]) ->log('Started impersonating user ' . $targetUser->full_name); return $session; } /** * Stop an impersonation session. */ public function stop(ImpersonationSession $session, string $endReason = 'manual'): void { $session->update([ 'ended_at' => now(), 'end_reason' => $endReason, ]); $this->clearCacheSession($session->admin_id, $session->target_user_id); activity('admin') ->causedBy(User::find($session->admin_id)) ->performedOn(User::find($session->target_user_id)) ->event('admin.impersonation.stopped') ->withProperties([ 'session_id' => $session->id, 'end_reason' => $endReason, 'actions_count' => $session->actions_count, ]) ->log('Stopped impersonating user (reason: ' . $endReason . ')'); } /** * Get active session for an admin user. */ public function getActiveSessionForAdmin(User $admin): ?ImpersonationSession { return ImpersonationSession::where('admin_id', $admin->id) ->active() ->first(); } /** * Get active session for a target user. */ public function getActiveSessionForTarget(User $targetUser): ?ImpersonationSession { return ImpersonationSession::where('target_user_id', $targetUser->id) ->active() ->first(); } /** * Check if a given admin is currently impersonating a specific user. * Used by middleware for fast validation via Redis. */ public function validateRequest(string $adminId, string $targetUserId, string $ipAddress): ?ImpersonationSession { $cacheKey = self::REDIS_PREFIX . $adminId . ':' . $targetUserId; $sessionId = Cache::get($cacheKey); if (! $sessionId) { return null; } $session = ImpersonationSession::find($sessionId); if (! $session || $session->ended_at !== null) { $this->clearCacheSession($adminId, $targetUserId); return null; } // IP pinning check if ($session->ip_address !== $ipAddress) { Log::warning('Impersonation IP mismatch', [ 'session_id' => $session->id, 'expected_ip' => $session->ip_address, 'actual_ip' => $ipAddress, ]); $this->stop($session, 'ip_changed'); return null; } // Sliding TTL: extend cache and DB expiry $newExpiry = now()->addSeconds(self::SESSION_TTL_SECONDS); Cache::put($cacheKey, $sessionId, $newExpiry); $session->update(['expires_at' => $newExpiry]); return $session; } /** * Increment the actions count for a session. */ public function incrementActionsCount(ImpersonationSession $session): void { $session->increment('actions_count'); } /** * Kill all active impersonation sessions (emergency). */ public function killAll(): int { $sessions = ImpersonationSession::active()->get(); $count = 0; foreach ($sessions as $session) { $this->stop($session, 'admin_kill_all'); $count++; } return $count; } // ─── Private helpers ─── private function verifyMfaCode(User $user, string $code, MfaMethod $method): void { match ($method) { MfaMethod::TOTP => $this->verifyTotpCode($user, $code), MfaMethod::EMAIL => $this->verifyEmailCode($user, $code), MfaMethod::BACKUP_CODE => $this->verifyBackupCode($user, $code), }; } private function verifyTotpCode(User $user, string $code): void { if (! $user->mfa_secret) { abort(403, 'TOTP is not configured.'); } $secret = decrypt($user->mfa_secret); if (! $this->google2fa->verifyKey($secret, $code, 1)) { abort(403, 'Invalid MFA code.'); } } private function verifyEmailCode(User $user, string $code): void { $record = MfaEmailCode::where('user_id', $user->id) ->where('code', $code) ->where('used', false) ->where('expires_at', '>', now()) ->first(); if (! $record) { abort(403, 'Invalid or expired email code.'); } $record->update(['used' => true]); } private function verifyBackupCode(User $user, string $code): void { $normalizedCode = strtoupper(str_replace([' ', '-'], '', $code)); $backupCodes = MfaBackupCode::where('user_id', $user->id) ->where('used', false) ->get(); foreach ($backupCodes as $backupCode) { if (Hash::check($code, $backupCode->code_hash) || Hash::check($normalizedCode, $backupCode->code_hash)) { $backupCode->update([ 'used' => true, 'used_at' => now(), ]); return; } } abort(403, 'Invalid backup code.'); } private function storeCacheSession(ImpersonationSession $session): void { $cacheKey = self::REDIS_PREFIX . $session->admin_id . ':' . $session->target_user_id; Cache::put($cacheKey, $session->id, now()->addSeconds(self::SESSION_TTL_SECONDS)); } private function clearCacheSession(string $adminId, string $targetUserId): void { $cacheKey = self::REDIS_PREFIX . $adminId . ':' . $targetUserId; Cache::forget($cacheKey); } }