laravelpusherlaravel-sanctumlaravel-echolaravel-reverb

Why am I getting a 401 Unauthorized on Laravel Echo's authentication to a Private Channel?


I'm building a template project for myself, and I've got Reverb and Echo working as intended with regular channels, but when using private channels, the /broadcasting/auth route returns a 401 Unauthorized with {"message":"Unauthenticated."} as its body. I've been searching and debugging for a few hours so far, and none of the suggestions I found online completely fixed my issues.
I've set the api and auth:sanctum middleware on the ->withBroadcasting() in the bootstrap/app.php:

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        api: __DIR__ . '/../routes/api.php',
        commands: __DIR__ . '/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware
            ->web(append: [
                HandleInertiaRequests::class,
                AddLinkHeadersForPreloadedAssets::class,
            ])
            ->trustProxies('*')
            ->validateCsrfTokens();
    })
    ->withBroadcasting('/../routes/channels.php', attributes: ['middleware' => ['api', 'auth:sanctum']])
    ->withExceptions()
    ->create();

My channels.php is empty, and the channel I'm trying to subscribe to is set using the BroadcastsEvents model trait on my User model:

class User extends Authenticatable
{
    use HasApiTokens;
    use HasFactory;
    use HasProfilePhoto;
    use Notifiable;
    use TwoFactorAuthenticatable;
    use BroadcastsEvents;

    ...

    public function broadcastOn($event): PrivateChannel
    {
        return new PrivateChannel('users.' . $this->id);
    }
}

My Echo is configured like so:

window.Echo = new Echo({
  broadcaster: 'reverb',
  key: import.meta.env.VITE_REVERB_APP_KEY,
  wsHost: import.meta.env.VITE_REVERB_HOST,
  wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
  wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
  forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
  enabledTransports: ['ws', 'wss'],
  encrypted: true,
  authorizer: (channel, options) => {
    console.log('a');
    return {
      authorize: (socketId, callback) => {
        console.log('b');
        axios
          .post(
            '/broadcasting/auth',
            {
              socket_id: socketId,
              channel_name: channel.name,
            },
            {
              withCredentials: true,
              headers: {
                Accept: 'application/json',
              },
            }
          )
          .then(response => {
            callback(false, response.data);
          })
          .catch(error => {
            callback(true, error);
          });
      },
    };
  },
});

I'm subscribing to the event in an onMounted from my frontend:

    onMounted(() => {
      if (user.value) {
        window.Echo.private(`users.${user.value.id}`).listen(
          '.UserUpdated',
          (event: { model: UserData }) => {
            console.log('Received user update from reverb!', event);
          }
        );
      }
    });

Furthermore I'm using the Jetstream starter kit without any impactful changes to the default configs. The user making the request is authenticated to the regular frontend routes just fine.
The project files are available here in case I missed anything: https://github.com/dannypas00/laravel-template/tree/reverb

I tried following the official laravel sanctum documentation on private broadcasting: https://laravel.com/docs/11.x/sanctum#authorizing-private-broadcast-channels
This didn't help much as I still receive 401 errors.


Solution

  • I discovered that my setup works as long as I add the statefulApi middleware. Also I had a typo where I didn't use base_path (or DIR) in the route path. Updating the bootstrap/app.php to this fixed the issues and I can now use private channels:

    return Application::configure(basePath: dirname(__DIR__))
        ->withRouting(
            web: base_path('routes/web.php'),
            api: base_path('routes/api.php'),
            commands: base_path('routes/console.php'),
            health: '/up',
        )
        ->withMiddleware(function (Middleware $middleware) {
            $middleware
                ->web(append: [
                    HandleInertiaRequests::class,
                    AddLinkHeadersForPreloadedAssets::class,
                ])
                ->statefulApi()
                ->trustProxies('*')
                ->validateCsrfTokens();
        })
        ->withBroadcasting(base_path('routes/channels.php'), attributes: ['middleware' => ['api', 'auth:sanctum']])
        ->withExceptions()
        ->create();