diff --git a/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php b/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php new file mode 100644 index 0000000000..813b0efa9f --- /dev/null +++ b/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php @@ -0,0 +1,90 @@ +getStatusCode() < 400) { + return $response; + } + + $user = Auth::user(); + $channelName = $request->input('channel_name'); + $channelInfo = $this->parseChannelInfo($channelName); + + Log::error('Broadcast auth failed', [ + 'status' => $response->getStatusCode(), + 'user_id' => $user?->id, + 'user_type' => $user ? get_class($user) : null, + 'user_is_anonymous' => $user && method_exists($user, 'isAnonymous') ? $user->isAnonymous : null, + 'has_session' => $request->hasSession(), + 'session_id' => $request->session()?->getId(), + 'channel_name' => $channelName, + 'channel_type' => $channelInfo['type'], + 'channel_resource_id' => $channelInfo['id'], + 'user_channel_mismatch' => $channelInfo['type'] === 'User' && $user && $channelInfo['id'] + ? (string) $user->id !== (string) $channelInfo['id'] + : null, + 'cookie_present' => $request->hasCookie(config('session.cookie')), + 'ip' => $request->ip(), + 'origin' => $request->header('Origin'), + 'referer' => $request->header('Referer'), + 'user_agent' => $request->userAgent(), + 'socket_id' => $request->input('socket_id'), + 'response_body' => $this->getResponseBody($response), + 'timestamp' => now()->toIso8601String(), + ]); + + return $response; + } + + private function parseChannelInfo(?string $channelName): array + { + if (!$channelName) { + return ['type' => null, 'id' => null]; + } + // Strip tenant prefix: tenant_X.ProcessMaker.Models.User.14 -> ProcessMaker.Models.User.14 + $channel = preg_replace('/^tenant_\d+\./', '', $channelName); + if (preg_match('/ProcessMaker\.Models\.User\.(\d+)/', $channel, $m)) { + return ['type' => 'User', 'id' => $m[1]]; + } + if (preg_match('/ProcessMaker\.Models\.ProcessRequest\.(\d+)/', $channel, $m)) { + return ['type' => 'ProcessRequest', 'id' => $m[1]]; + } + if (preg_match('/ProcessMaker\.Models\.ProcessRequestToken\.(\d+)/', $channel, $m)) { + return ['type' => 'ProcessRequestToken', 'id' => $m[1]]; + } + + return ['type' => 'other', 'id' => null]; + } + + private function getResponseBody($response): ?string + { + $content = $response->getContent(); + if (is_string($content) && strlen($content) < 500) { + return $content; + } + + return $content ? '[truncated]' : null; + } +} diff --git a/ProcessMaker/Providers/BroadcastServiceProvider.php b/ProcessMaker/Providers/BroadcastServiceProvider.php index 233252f282..62bf075237 100644 --- a/ProcessMaker/Providers/BroadcastServiceProvider.php +++ b/ProcessMaker/Providers/BroadcastServiceProvider.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\ServiceProvider; +use ProcessMaker\Http\Middleware\BroadcastAuthDebug; class BroadcastServiceProvider extends ServiceProvider { @@ -14,7 +15,7 @@ class BroadcastServiceProvider extends ServiceProvider */ public function boot() { - Broadcast::routes(['middleware'=>['web', 'auth:anon']]); + Broadcast::routes(['middleware' => ['web', 'auth:web,anon', BroadcastAuthDebug::class]]); require base_path('routes/channels.php'); } } diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 0ea799903c..137403b912 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -340,7 +340,17 @@ if (userID) { } if (window.Processmaker && window.Processmaker.broadcasting) { - const config = window.Processmaker.broadcasting; + const config = { ...window.Processmaker.broadcasting }; + + // Ensure auth request goes to Laravel app with cookies (reduces 403 from CORS/cookie issues) + if (!config.authEndpoint) { + config.authEndpoint = `${window.location.origin}/broadcasting/auth`; + } + config.auth = config.auth || {}; + config.auth.headers = config.auth.headers || {}; + if (config.auth.withCredentials === undefined) { + config.auth.withCredentials = true; + } if (config.broadcaster == "pusher") { window.Pusher = require("pusher-js"); @@ -348,6 +358,39 @@ if (window.Processmaker && window.Processmaker.broadcasting) { } window.Echo = new TenantAwareEcho(config); + + // Prevent private channel subscription when no user or wrong user (avoids 403 on /broadcasting/auth) + const noOpChannel = { + listen: () => noOpChannel, + notification: () => noOpChannel, + stopListening: () => noOpChannel, + listenForWhisper: () => noOpChannel, + error: () => noOpChannel, + }; + const originalPrivate = window.Echo.private.bind(window.Echo); + const getUserId = () => + window.Processmaker?.userId || + window.ProcessMaker?.user?.id || + document.head.querySelector('meta[name="user-id"]')?.content; + + // Extract user id from ProcessMaker.Models.User.{id} channel (with optional tenant prefix) + const getUserIdFromChannel = (ch) => { + const match = ch.match(/ProcessMaker\.Models\.User\.(\d+)/); + return match ? match[1] : null; + }; + + window.Echo.private = (channel, ...args) => { + const currentUserId = String(getUserId() || ""); + if (!currentUserId) { + return noOpChannel; + } + // Block subscription to another user's channel (would always 403) + const channelUserId = getUserIdFromChannel(channel); + if (channelUserId && channelUserId !== currentUserId) { + return noOpChannel; + } + return originalPrivate(channel, ...args); + }; } if (userID) { diff --git a/routes/channels.php b/routes/channels.php index 64d6c9651d..91b7eaf75b 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -11,16 +11,25 @@ | */ +use ProcessMaker\Models\AnonymousUser; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; Broadcast::channel('ProcessMaker.Models.User.{id}', function ($user, $id) { + if (!$user || $user instanceof AnonymousUser) { + return false; + } + return (int) $user->id === (int) $id; }); Broadcast::channel('ProcessMaker.Models.ProcessRequest.{id}', function ($user, $id) { + if (!$user || $user instanceof AnonymousUser) { + return false; + } + if ($id === 'undefined' || $user === 'undefined') { - return; + return false; } if ($user->is_administrator) { @@ -35,6 +44,10 @@ }); Broadcast::channel('ProcessMaker.Models.ProcessRequestToken.{id}', function ($user, $id) { + if (!$user || $user instanceof AnonymousUser) { + return false; + } + if ($user->is_administrator) { return true; } @@ -45,9 +58,17 @@ }); Broadcast::channel('test.status', function ($user) { + if (!$user || $user instanceof AnonymousUser) { + return false; + } + return true; }); Broadcast::channel('ProcessMaker.Models.Process.{processId}.Language.{language}', function ($user, $processId, $language) { + if (!$user || $user instanceof AnonymousUser) { + return false; + } + return true; }); diff --git a/routes/web.php b/routes/web.php index ed58abb720..9ba7563172 100644 --- a/routes/web.php +++ b/routes/web.php @@ -215,9 +215,6 @@ Route::get('tasks/update_variable/{token_abe}', [TaskController::class, 'updateVariable'])->name('tasks.abe.update'); }); -// Add our broadcasting routes -Broadcast::routes(); - // Authentication Routes... Route::get('login', [LoginController::class, 'showLoginForm'])->name('login'); Route::post('login', [LoginController::class, 'loginWithIntendedCheck']);