Repository: laravel/reverb (php)

Issue:
Reverb process crashes if no $auth parameter is provided
### Reverb Version

v1.0.0-beta3

### Laravel Version

v11.0.6

### PHP Version

8.3

### Description

After putting reverb into production and replacing soketi everything worked fine for an hour and then I started receiving these errors which crash the reverb process:

2024-03-15 02:16:37] production.ERROR: Laravel\Reverb\Protocols\Pusher\Channels\PrivateChannel::verify(): Argument #2 ($auth) must be of type string, null given, called in /home/forge/xxxxxx/vendor/laravel/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php on line 16 {"exception":"[object] (TypeError(code: 0): Laravel\\Reverb\\Protocols\\Pusher\\Channels\\PrivateChannel::verify(): 

I have fixed the error temporarily by modifying the signature of the verify method to match the subscribe method however I am unsure if this is the correct way to fix this issue:

 protected function verify(Connection $connection, ?string $auth = null, ?string $data = null): bool

### Steps To Reproduce

Pass a null $auth parameter while trying to connect to a private channel


Discussion:
Can you put more information?
Hey there, thanks for reporting this issue.

We'll need more info and/or code to debug this further. Can you please create a repository with the command below, commit the code that reproduces the issue *as one separate commit* on the main/master branch and share the repository here?

Please make sure that you have the [latest version of the Laravel installer](https://github.com/laravel/installer) in order to run this command. Please also make sure you have both Git & [the GitHub CLI tool](https://cli.github.com/) properly set up.

    laravel new bug-report --github="--public"

Do not amend and create a separate commit with your custom changes. After you've posted the repository, we'll try to reproduce the issue.

Thanks!
Sorry, I have tried to reproduce the issue from within laravel but I am unsure on how to trigger it during local testing. In production I have thousands of clients connecting to the websocket server and I am guessing that under some specific condition the $auth variable is left unasigned and gets set to null because thats the default value. My only solution would be to undo my changes in production and perhaps capture the info from the connecting client, they might be using an outdated installation of Echo or something like that. In any case, feel free to close this issue as I am unable to reproduce it locally.
I am also experiencing the same issue but in local environment.

 `[2024-03-17 14:20:13] local.ERROR: Laravel\Reverb\Protocols\Pusher\Channels\PrivateChannel::verify(): Argument #2 ($auth) must be of type string, null given, called in /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php on line 16 {"exception":"[object] (TypeError(code: 0): Laravel\\Reverb\\Protocols\\Pusher\\Channels\\PrivateChannel::verify(): Argument #2 ($auth) must be of type string, null given, called in /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php on line 16 at /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php:24)
[stacktrace]
#0 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php(16): Laravel\\Reverb\\Protocols\\Pusher\\Channels\\PrivateChannel->verify()
#1 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/EventHandler.php(62): Laravel\\Reverb\\Protocols\\Pusher\\Channels\\PrivateChannel->subscribe()
#2 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/EventHandler.php(29): Laravel\\Reverb\\Protocols\\Pusher\\EventHandler->subscribe()
#3 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/Server.php(56): Laravel\\Reverb\\Protocols\\Pusher\\EventHandler->handle()
#4 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Protocols/Pusher/Http/Controllers/PusherController.php(34): Laravel\\Reverb\\Protocols\\Pusher\\Server->message()
#5 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/ratchet/rfc6455/src/Messaging/MessageBuffer.php(248): Laravel\\Reverb\\Protocols\\Pusher\\Http\\Controllers\\PusherController->Laravel\\Reverb\\Protocols\\Pusher\\Http\\Controllers\\{closure}()
#6 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/ratchet/rfc6455/src/Messaging/MessageBuffer.php(194): Ratchet\\RFC6455\\Messaging\\MessageBuffer->processData()
#7 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/evenement/evenement/src/EventEmitterTrait.php(143): Ratchet\\RFC6455\\Messaging\\MessageBuffer->onData()
#8 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/react/stream/src/Util.php(71): Evenement\\EventEmitter->emit()
#9 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/evenement/evenement/src/EventEmitterTrait.php(143): React\\Stream\\Util::React\\Stream\\{closure}()
#10 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/react/stream/src/DuplexResourceStream.php(196): Evenement\\EventEmitter->emit()
#11 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/react/event-loop/src/StreamSelectLoop.php(246): React\\Stream\\DuplexResourceStream->handleData()
#12 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/react/event-loop/src/StreamSelectLoop.php(213): React\\EventLoop\\StreamSelectLoop->waitForStreamActivity()
#13 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Servers/Reverb/Http/Server.php(39): React\\EventLoop\\StreamSelectLoop->run()
#14 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/reverb/src/Servers/Reverb/Console/Commands/StartServer.php(70): Laravel\\Reverb\\Servers\\Reverb\\Http\\Server->start()
#15 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Laravel\\Reverb\\Servers\\Reverb\\Console\\Commands\\StartServer->handle()
#16 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Container/Util.php(41): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#17 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\\Container\\Util::unwrapIfClosure()
#18 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod()
#19 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Container/Container.php(662): Illuminate\\Container\\BoundMethod::call()
#20 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Console/Command.php(212): Illuminate\\Container\\Container->call()
#21 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/symfony/console/Command/Command.php(279): Illuminate\\Console\\Command->execute()
#22 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Console/Command.php(181): Symfony\\Component\\Console\\Command\\Command->run()
#23 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/symfony/console/Application.php(1049): Illuminate\\Console\\Command->run()
#24 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/symfony/console/Application.php(318): Symfony\\Component\\Console\\Application->doRunCommand()
#25 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/symfony/console/Application.php(169): Symfony\\Component\\Console\\Application->doRun()
#26 /opt/lampp/htdocs/Wingu-2.0-Backend/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(196): Symfony\\Component\\Console\\Application->run()
#27 /opt/lampp/htdocs/Wingu-2.0-Backend/artisan(35): Illuminate\\Foundation\\Console\\Kernel->handle()
#28 {main}
"} `

added this to api route file:

`Broadcast::routes(['middleware' => ['auth:sanctum']]);`

the channel : 

`Broadcast::channel('mpesa-credentials.{user_id}', function ($user, $id) {
    return $user->id === $id; });`

frontend echo configuration:

`import Echo from "laravel-echo";
import axios from '@/lib/axios';
const EchoConfig = () => {

window.Pusher = require('pusher-js');
 window.Echo = new Echo({

        broadcaster: 'reverb',
        key: process.env.NEXT_PUBLIC_REVERB_APP_KEY,
        wsHost: window.location.hostname,
        wsPort: process.env.NEXT_PUBLIC_REVERB_PORT,
        wssPort: process.env.NEXT_PUBLIC_REVERB_PORT,
        forceTLS: (process.env.NEXT_PUBLIC_REVERB_SCHEME ?? 'https') === 'https',
        enabledTransports: ['ws', 'wss'],
        encrypted: false,      
       authorizer: (channel, options) => {
        return {
            authorize: (socketId, callback) => {
                axios.post('/api/broadcasting/auth', {
                    socket_id: socketId,
                    channel_name: channel.name
                })
                .then(response => {
                    console.log('success:'+response.data)
                    callback(false, response.data);
                })
                .catch(error => {
                        console.log('error:'+error)
                    callback(true, error);
                });
            }
        };
    },

})}
export default EchoConfig;`
Hey @KirinyetBrian can you share a screenshot of the WebSocket in the network panel expanding the detail of the `pusher:subscribe` event?

Patch:
diff --git a/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php b/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php
--- a/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php
+++ b/src/Protocols/Pusher/Channels/Concerns/InteractsWithPrivateChannels.php
@@ -21,7 +21,7 @@ public function subscribe(Connection $connection, ?string $auth = null, ?string
     /**
      * Deteremine whether the given authentication token is valid.
      */
-    protected function verify(Connection $connection, string $auth, ?string $data = null): bool
+    protected function verify(Connection $connection, ?string $auth = null, ?string $data = null): bool
     {
         $signature = "{$connection->id()}:{$this->name()}";
 


Test patch:
diff --git a/tests/Feature/Protocols/Pusher/Reverb/ServerTest.php b/tests/Feature/Protocols/Pusher/Reverb/ServerTest.php
--- a/tests/Feature/Protocols/Pusher/Reverb/ServerTest.php
+++ b/tests/Feature/Protocols/Pusher/Reverb/ServerTest.php
@@ -405,3 +405,27 @@
 
     expect(channels()->all())->toHaveCount(0);
 });
+
+it('fails to subscribe to private channel with no auth token', function () {
+    $response = send([
+        'event' => 'pusher:subscribe',
+        'data' => [
+            'channel' => 'private-test-channel',
+            'auth' => null,
+        ],
+    ], connect());
+
+    expect($response)->toBe('{"event":"pusher:error","data":"{\"code\":4009,\"message\":\"Connection is unauthorized\"}"}');
+});
+
+it('fails to subscribe to presence channel with no auth token', function () {
+    $response = send([
+        'event' => 'pusher:subscribe',
+        'data' => [
+            'channel' => 'presence-test-channel',
+            'auth' => null,
+        ],
+    ], connect());
+
+    expect($response)->toBe('{"event":"pusher:error","data":"{\"code\":4009,\"message\":\"Connection is unauthorized\"}"}');
+});
diff --git a/tests/Unit/Protocols/Pusher/Channels/PrivateChannelTest.php b/tests/Unit/Protocols/Pusher/Channels/PrivateChannelTest.php
--- a/tests/Unit/Protocols/Pusher/Channels/PrivateChannelTest.php
+++ b/tests/Unit/Protocols/Pusher/Channels/PrivateChannelTest.php
@@ -1,5 +1,6 @@
 <?php
 
+use Laravel\Reverb\Protocols\Pusher\Channels\PresenceChannel;
 use Laravel\Reverb\Protocols\Pusher\Channels\PrivateChannel;
 use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelConnectionManager;
 use Laravel\Reverb\Protocols\Pusher\Exceptions\ConnectionUnauthorized;
@@ -54,3 +55,15 @@
 
     $channel->subscribe($this->connection, 'invalid-signature');
 })->throws(ConnectionUnauthorized::class);
+
+it('fails to subscribe to a private channel with no auth token', function () {
+    $channel = new PrivateChannel('private-test-channel');
+
+    $channel->subscribe($this->connection, null);
+})->throws(ConnectionUnauthorized::class);
+
+it('fails to subscribe to a presence channel with no auth token', function () {
+    $channel = new PresenceChannel('presence-test-channel');
+
+    $channel->subscribe($this->connection, null);
+})->throws(ConnectionUnauthorized::class);