diff --git a/src/Bootstrappers/BroadcastingConfigBootstrapper.php b/src/Bootstrappers/BroadcastingConfigBootstrapper.php index 9fffc7fe..2afd61a7 100644 --- a/src/Bootstrappers/BroadcastingConfigBootstrapper.php +++ b/src/Bootstrappers/BroadcastingConfigBootstrapper.php @@ -91,27 +91,11 @@ class BroadcastingConfigBootstrapper implements TenancyBootstrapper // contract gets the same tenant broadcaster that the manager uses, instead of the stale central one. // The closure runs immediately (the extended singleton is already resolved), and it's also what makes // channel auth work in tenant context -- the broadcaster resolved here gets cached as the tenant - // manager's default driver and receives the central channel auth closures (see below). + // manager's default driver and receives the central broadcaster's auth state (see copyAuthState()). $this->app->extend(BroadcasterContract::class, function (BroadcasterContract $centralBroadcaster) { $tenantBroadcaster = $this->app->make(BroadcastManager::class)->connection(); - // The newly resolved broadcaster doesn't have any channel auth closures registered, so the - // closures registered in central context (e.g. in routes/channels.php) have to be passed to it - // manually, otherwise, Broadcast::auth() would throw a 403 for those channels. - // Since Laravel only ever uses the default broadcaster's channel auth closures for broadcasting auth, - // we only have to pass the channel closures to the default broadcaster. - // - // The channel() and getChannels() methods aren't part of the Broadcaster contract -- they come - // from the abstract Broadcaster class, so the closures can only be copied between broadcasters extending it - // (which all of Laravel's default broadcasters, e.g. PusherBroadcaster, do). - if ($centralBroadcaster instanceof Broadcaster && $tenantBroadcaster instanceof Broadcaster) { - // invade() because the channel options can't be retrieved through any of the broadcaster's public methods - $channelOptions = invade($centralBroadcaster)->channelOptions; - - foreach ($centralBroadcaster->getChannels() as $channel => $callback) { - $tenantBroadcaster->channel($channel, $callback, $channelOptions[$channel] ?? []); - } - } + $this->copyAuthState($centralBroadcaster, $tenantBroadcaster); return $tenantBroadcaster; }); @@ -122,6 +106,32 @@ class BroadcastingConfigBootstrapper implements TenancyBootstrapper Broadcast::clearResolvedInstance(); } + /** + * Copy the auth state (the channel auth closures, their options, and the authenticated user + * callback) from one broadcaster to another. A freshly resolved broadcaster has no auth state, + * so without the copying, channel auth and user auth would stop working (403) in tenant context. + * + * The auth state is stored on the abstract Broadcaster class, not in the Broadcaster + * contract, and it's stored in protected properties. Because of that, we have + * to check that both broadcasters are instances of the abstract Broadcaster class and + * use invade() to access the protected properties (for the $channels property, there + * is a public accessor -- getChannels() -- but since invade is already used here, + * we access the property directly for consistency). + */ + protected function copyAuthState(BroadcasterContract $from, BroadcasterContract $to): void + { + if (! $from instanceof Broadcaster || ! $to instanceof Broadcaster) { + return; + } + + $fromState = invade($from); + $toState = invade($to); + + $toState->channels = $fromState->channels; + $toState->channelOptions = $fromState->channelOptions; + $toState->authenticatedUserCallback = $fromState->authenticatedUserCallback; + } + public function revert(): void { // Revert the bound BroadcastManager and Broadcaster singletons back to their original state