From 32311fce2b1ddc08914cd8ebab8b38524b50f9c2 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 18 Feb 2026 14:57:39 -0400 Subject: [PATCH 1/3] Add BroadcastAuthDebug middleware for logging failed broadcast auth requests - Introduced BroadcastAuthDebug middleware to log failed broadcast authentication attempts (HTTP status codes 401, 403, 500) for debugging purposes. - Updated BroadcastServiceProvider to include the new middleware in the broadcast routes. - Enhanced private channel subscription logic in bootstrap.js to prevent 403 errors when no user is authenticated. - Updated channels.php to ensure anonymous users are denied access to specific channels. --- .../Http/Middleware/BroadcastAuthDebug.php | 45 +++++++++++++++++++ .../Providers/BroadcastServiceProvider.php | 4 +- resources/js/bootstrap.js | 20 +++++++++ routes/channels.php | 23 +++++++++- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 ProcessMaker/Http/Middleware/BroadcastAuthDebug.php diff --git a/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php b/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php new file mode 100644 index 0000000000..20210e2b4f --- /dev/null +++ b/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php @@ -0,0 +1,45 @@ +getStatusCode() >= 400) { + Log::error('Broadcast auth failed', [ + 'status' => $response->getStatusCode(), + 'user_id' => $user?->id, + 'user_type' => $user ? get_class($user) : null, + 'has_session' => $request->hasSession(), + 'session_id' => $request->session()?->getId(), + 'channel' => $request->input('channel_name'), + 'cookie_present' => $request->hasCookie(config('session.cookie')), + 'ip' => $request->ip(), + 'timestamp' => now()->toIso8601String(), + ]); + } + + return $response; + } +} diff --git a/ProcessMaker/Providers/BroadcastServiceProvider.php b/ProcessMaker/Providers/BroadcastServiceProvider.php index 233252f282..28d7476065 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,8 @@ class BroadcastServiceProvider extends ServiceProvider */ public function boot() { - Broadcast::routes(['middleware'=>['web', 'auth:anon']]); + //auth:web,anon is needed to allow anonymous users to listen to channels + 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..63efd45639 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -348,6 +348,26 @@ if (window.Processmaker && window.Processmaker.broadcasting) { } window.Echo = new TenantAwareEcho(config); + + // Option 3: Prevent private channel subscription when no user (avoids 403 on /broadcasting/auth) + const noOpChannel = { + listen: () => noOpChannel, + notification: () => noOpChannel, + stopListening: () => noOpChannel, + listenForWhisper: () => 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; + + window.Echo.private = (channel, ...args) => { + if (!getUserId()) { + 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; }); From b37579662a504566a180eac712a6fb15a12fc877 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Fri, 20 Feb 2026 16:58:28 -0400 Subject: [PATCH 2/3] Updated broadcasting configuration to ensure authentication requests are directed to the Laravel app with cookies, reducing CORS-related 403 errors. - Added logic to handle user ID extraction from private channels, preventing subscription to channels of other users. - Improved error handling for private channel subscriptions when no user is authenticated. --- resources/js/bootstrap.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 63efd45639..b1b97380d7 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -340,7 +340,16 @@ 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 || {}; + if (config.auth.withCredentials === undefined) { + config.auth.withCredentials = true; + } if (config.broadcaster == "pusher") { window.Pusher = require("pusher-js"); @@ -349,12 +358,13 @@ if (window.Processmaker && window.Processmaker.broadcasting) { window.Echo = new TenantAwareEcho(config); - // Option 3: Prevent private channel subscription when no user (avoids 403 on /broadcasting/auth) + // 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 = () => @@ -362,8 +372,20 @@ if (window.Processmaker && window.Processmaker.broadcasting) { 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) => { - if (!getUserId()) { + 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); From 92b54ebe04bd0c36aac05f0bc89291cd06d305e4 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Sat, 21 Feb 2026 15:33:23 -0400 Subject: [PATCH 3/3] Enhanced BroadcastAuthDebug middleware for improved error logging and channel information parsing - Updated BroadcastAuthDebug middleware to log additional details on failed broadcast authentication attempts, including user anonymity and channel type. --- .../Http/Middleware/BroadcastAuthDebug.php | 71 +++++++++++++++---- .../Providers/BroadcastServiceProvider.php | 1 - resources/js/bootstrap.js | 1 + routes/web.php | 3 - 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php b/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php index 20210e2b4f..813b0efa9f 100644 --- a/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php +++ b/ProcessMaker/Http/Middleware/BroadcastAuthDebug.php @@ -24,22 +24,67 @@ public function handle($request, Closure $next) return $response; } + if ($response->getStatusCode() < 400) { + return $response; + } + $user = Auth::user(); + $channelName = $request->input('channel_name'); + $channelInfo = $this->parseChannelInfo($channelName); - if ($response->getStatusCode() >= 400) { - Log::error('Broadcast auth failed', [ - 'status' => $response->getStatusCode(), - 'user_id' => $user?->id, - 'user_type' => $user ? get_class($user) : null, - 'has_session' => $request->hasSession(), - 'session_id' => $request->session()?->getId(), - 'channel' => $request->input('channel_name'), - 'cookie_present' => $request->hasCookie(config('session.cookie')), - 'ip' => $request->ip(), - 'timestamp' => now()->toIso8601String(), - ]); - } + 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 28d7476065..62bf075237 100644 --- a/ProcessMaker/Providers/BroadcastServiceProvider.php +++ b/ProcessMaker/Providers/BroadcastServiceProvider.php @@ -15,7 +15,6 @@ class BroadcastServiceProvider extends ServiceProvider */ public function boot() { - //auth:web,anon is needed to allow anonymous users to listen to channels 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 b1b97380d7..137403b912 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -347,6 +347,7 @@ if (window.Processmaker && window.Processmaker.broadcasting) { 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; } 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']);