header('X-Impersonate-User'); if (! $targetUserId) { return $next($request); } /** @var User|null $admin */ $admin = $request->user(); if (! $admin) { return response()->json(['message' => 'Authentication required.'], 401); } // Block sensitive routes during impersonation if ($this->isSensitiveRoute($request)) { return response()->json([ 'message' => 'This action is not allowed during impersonation.', ], 403); } // Validate impersonation session via Redis $session = $this->impersonationService->validateRequest( $admin->id, $targetUserId, $request->ip(), ); if (! $session) { return response()->json([ 'message' => 'Impersonation session is invalid or has expired.', 'impersonation_ended' => true, ], 403); } // Load the target user $targetUser = User::find($targetUserId); if (! $targetUser) { return response()->json(['message' => 'Target user not found.'], 404); } // Store impersonation context in request attributes $request->attributes->set('impersonator', $admin); $request->attributes->set('impersonation_session', $session); // Swap auth context — the rest of the request sees the target user app('auth')->setUser($targetUser); // Tag all log entries with impersonation context Log::shareContext([ 'impersonated_by' => $admin->id, 'impersonation_session_id' => $session->id, ]); // Re-bind Sentry auth-scope tags after the user swap. The // Authenticated event already fired with the admin; AuthScopeContextListener // tagged the admin's user_id/actor_type. We now overwrite both with // the target's data and add the impersonation.* invariants // (RFC-WS-7 §3.6) so captured events attribute correctly. $targetActorType = ActorType::resolve($targetUser, $request); configureScope(static function (Scope $scope) use ($admin, $targetUser, $session, $targetActorType): void { $scope->setUser([ 'id' => $targetUser->id, 'username' => $targetUser->id, ]); $scope->setTag('user_id', $targetUser->id); $scope->setTag('actor_type', $targetActorType->value); $scope->setTag('impersonation.active', 'true'); $scope->setTag('impersonation.impersonator_user_id', $admin->id); $scope->setTag('impersonation.session_id', $session->id); }); // Increment actions count $this->impersonationService->incrementActionsCount($session); return $next($request); } private function isSensitiveRoute(Request $request): bool { // Get path relative to API prefix (strip api/v1/) $path = $request->path(); $path = preg_replace('#^api/v1/#', '', $path); // Block profile mutations but allow GET (viewing) if (str_starts_with($path, 'me/profile') && $request->method() !== 'GET') { return true; } foreach (self::BLOCKED_ROUTE_PREFIXES as $prefix) { if (str_starts_with($path, $prefix)) { return true; } } return false; } }