beyondcode / laravel-websockets

Websockets for Laravel. Done right.
https://beyondco.de/docs/laravel-websockets
MIT License
5.08k stars 628 forks source link

Multi-tenancy Invalid signature on Private channels #184

Closed SolStis86 closed 5 years ago

SolStis86 commented 5 years ago

When receiving and authentication payload from a private or presence channel, the payload should be in the format key:signature. However, whats actually returned from the auth endpoint is something like this:

{"auth":":ce25a333287f30d67b36e486f6e60b59b0a0e55a7df62d209ad67766ef170ab5"}

Note that only the signature is present, not the key.

From what i can see the auth string is generated in Pusher\Pusher@socket_auth (Ln:735) however it seems that the auth_key isnt being set ready for the response.

Im using a custom AppProvider that retrieves the config on a per tenant basis and the overrides the config for the pusher connection through middleware on every request.

My app provider is as follows:

namespace App\Websockets;

use BeyondCode\LaravelWebSockets\Apps\App;
use BeyondCode\LaravelWebSockets\Apps\AppProvider as AppProviderInterface;
use App\Models\System\Website;
use Illuminate\Support\Facades\Cache;

class AppProvider implements AppProviderInterface
{
    public function all(): array
    {
        return Website::all()
            ->map(function (Website $website) {
                return $this->transformWebsiteToApp($website);
            })
            ->toArray();
    }

    public function findById($appId): ?App
    {
        $website = Website::find($appId);

        if (! $website) {
            return null;
        }

        return $this->transformWebsiteToApp($website);
    }

    public function findByKey(string $appKey): ?App
    {
        $website = Website::where('key', $appKey)->first();

        if (! $website) {
            return null;
        }

        return $this->transformWebsiteToApp($website);
    }

    public function findBySecret(string $appSecret): ?App
    {
        $website = Website::where('secret', $appSecret)->first();

        if (! $website) {
            return null;
        }

        return $this->transformWebsiteToApp($website);
    }

    protected function transformWebsiteToApp(Website $website)
    {
        return (new App($website->id, $website->key, $website->secret))
            ->setName($website->uuid)
            ->enableClientMessages(true)
            ->enableStatistics(false);
    }
}

And the middleware:

namespace App\Http\Middleware;

use Closure;
use Hyn\Tenancy\Environment;

class SetupTenantConfig
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($website = app(Environment::class)->tenant()) {
            config([
                'broadcasting.connections.pusher' => [
                    'driver' => 'pusher',
                    'key' => $website->key,
                    'secret' => $website->secret,
                    'app_id' => $website->id,
                    'options' => [
//                        'cluster' => env('PUSHER_APP_CLUSTER'),
                        'encrypted' => true,
                        'host' => '127.0.0.1',
                        'port' => 6001,
                        'scheme' => 'http',
                        'auth_key' => $website->key,
                    ],
                ],
            ]);
        }

        return $next($request);
    }
}

Anyone else ran in to this issue and solved it? Thanks

SolStis86 commented 5 years ago

Ive manually overridden the response from the auth route with:

Route::post('broadcasting/auth', function () {
    $response = Broadcast::auth(request());
    $key = app(\Hyn\Tenancy\Environment::class)->tenant()->key;
    $response['auth'] = $key.$response['auth'];
    return $response;
})

And still receiving the InvalidSignatureException so the key not being set wasn't the issue. Im still at a loss as to why the signature is invalid. Any help would be greatly appreciated. Thanks

SolStis86 commented 5 years ago

Ok so it looks to be that the pusher instance used for generating the auth response has an empty config. Where about is this pusher instance set? This may be due to the config being set in the middleware being done later down the stack from where this instance is set.

SolStis86 commented 5 years ago

Solved!

  1. Rip out the native Laravel BroadcastingServiceProvider
  2. Replace with a custom one that isn't deferred
  3. Replace as follows:
    
    namespace App\Providers;

use Hyn\Tenancy\Environment; use Illuminate\Broadcasting\BroadcastManager; use Illuminate\Support\ServiceProvider; use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory; use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;

class BroadcastServiceProvider extends ServiceProvider { /**

Basically resolves the host name and applies the config accordingly before the Pusher instance is created in container.

Very much did a rubber duck on this one...

SolStis86 commented 5 years ago

34

adam-baliatico commented 5 years ago

@SolStis86 did you have any luck getting this to work with queued notifications? It appears the queue worker is not getting the proper pusher settings applied.

scramatte commented 3 years ago

Hello,

I've tried this with https://tenancyforlaravel.com/ lib. But I'm unable to get it working. Without BroadcastServiceProvider , broadcasting/auth route returns me OK 200 but invalid signature.

If I overwrite BroadcastServiceProvider , I got 403 on broadcasting/auth call.

Any idea?

Regards

scramatte commented 3 years ago

I've been able to get it working... The problem is the logic is little bite different with tenancyforlaravel ... You have tenancy bootstrappers

Instead overwrite BroadcastServiceProvider, I've mad a bootstrapper that achieve the same goal. What occurs is after register back the broadcast provider, you delete channels too. Basically you need to recreate channels , importing back routes/channels.php ...

Hope that helps somebody

class WebSocketsBootstrapper implements TenancyBootstrapper
{
    public function bootstrap(Tenant $tenant)
    {
        config([
            'broadcasting.connections.pusher' => [
                    'driver' => 'pusher',
                    'key' => $tenant->pusher_key,
                    'secret' => $tenant->pusher_secret,
                    'app_id' => $tenant->id,
                    'options' => [
                        'encrypted' => true,
                        'host' => '127.0.0.1',
                        'port' => 6001,
                        'scheme' => 'http',
                    ],
                ],
        ]);

        app()->singleton(BroadcastManager::class, function ($app) {
            return new BroadcastManager($app);
        });

        app()->singleton(BroadcasterContract::class, function ($app) {
            return $app->make(BroadcastManager::class)->connection();
        });

        app()->alias(
            BroadcastManager::class,
            BroadcastingFactory::class
        );

        require base_path('routes/tenant/channels.php');
...
a-ssassi-n commented 3 years ago

@scramatte I tried your code but still when I try to connect to a private channel the /broadcasting/auth connects to the central database instead of connecting to the tenant database to find the auth user.

I created the WebSocketsBootstrapper bootstrapper and registered it in config\tenancy.php bootstrappers like so: App\WebSocketsBootstrapper::class,

Is there anything that I am missing here?

mgouguasse commented 1 year ago

@scramatte please can you explain more your code and the files that you changed or import