Laravel Echo - Allow guests to connect to presence channel
Asked Answered
T

7

9

I am using laravel-echo-server to run Laravel Echo to broadcast events.

I have a user counter channel which shows all the users on the app. For this I am using a presence channel. This works fine for logged in users, but guests just never get connected.

I've setup the below in the BroadcastServiceProvider:

Broadcast::channel('global', function () { return ['name' => 'guest']; });

Which from what I can tell, should allow everyone in as 'guests'. I'm guessing there's some middleware or auth that's being checked before this that I need to disable for this channel.

Any help on getting all clients joining this presence channel would be much appreciated!

Tudela answered 11/4, 2017 at 9:23 Comment(0)
I
7

The other solutions didn't work for me for a guest presence channel, this is what I ended up with:

// routes/channels.php

<?php
use Illuminate\Auth\GenericUser;

/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Route::post('/custom/broadcast/auth/route', function () {
    $user = new GenericUser(['id' => microtime()]);

    request()->setUserResolver(function () use ($user) {
        return $user;
    });

    return Broadcast::auth(request());
});

Broadcast::channel('online.{uuid}', function ($user, $uuid) {
    return [
        'id' => $user->id,
        'uuid' => $uuid
    ];
});



Incapacitate answered 27/3, 2019 at 14:5 Comment(1)
Perfect simple solution!Gaylene
P
4

You may create a temporary user with factory(User::class)->make(...) and authenticate it with a middleware to use it as a guest.

Step 1: Creating the middleware

Run: php artisan make:middleware AuthenticateGuest

In app/Http/Middleware/AuthenticateGuest.php:

public function handle($request, Closure $next)
{
    Auth::login(factory(User::class)->make([
        'id' => (int) str_replace('.', '', microtime(true))
    ]));

    return $next($request);
}

Now setup the AuthenticateGuest middleware in Kernel.php.

In app\Http\Kernel.php:

protected $routeMiddleware = [
    ...
    'authenticate-guest' => \App\Http\Middleware\AuthenticateGuest::class,
];

Step 2: Setup Broadcast::channel route

In routes/channels.php:

Broadcast::channel('chatroom', function ($user) {
    return $user; // here will return the guest user object
});

More at: https://laravel.com/docs/8.x/broadcasting#authorizing-presence-channels

Pustule answered 8/1, 2018 at 11:23 Comment(5)
So just skip step 1 and adjust code in step 2 then?Heilbronn
Yes. I will updade my answer to keep it more clean. Thanks for the comment!Pustule
Great, just wanted to clarify so I could try it, didn't mean to nitpick.Heilbronn
But you don't even use the middleware? It is just created. You also need to register it in the boot method of your BroadcastServiceProvider. Else it wont be called.Bailee
This answer would have most likely worked for me if it included this middleware in the routes/web.php, like the following: Broadcast::routes(['middleware' => ['web', 'authenticate-guest']]);Questionary
T
2

For anyone looking for answers to this. It is indeed possible to auth guests into presence channels you just need to override the Broadcast::routes() from the service provider with your own.

As an example my presence channel 'global' accepts guests:

Route::post('/broadcasting/auth', function(Illuminate\Http\Request $req) { if($req->channel_name == 'presence-global'){return 'global';} return abort(403); });

This could be extended in various directions, or could continue to pass other presence and private channels through to the default Broadcast::auth method

Tudela answered 12/4, 2017 at 10:45 Comment(1)
And then, How do you join the room?Iceni
M
1

You can create your own auth guard and it's also pretty simple but more complex.

  1. Create a class which will implement Authenticable Interface.
  2. Create UserProvider.
  3. Create a new Guard.
  4. Register Guard and UserProvider in AuthServiceProvider.
  5. Add provider and guard in config/auth.php
  6. Use your new guard.

Advantages

  • You don't have to modify auth endpoint
  • You don't have to change default guard
  • You base on Laravel Auth system
  • Keep support of multiple tabs in the browser
  • Can be used with web guard at the same time
  • Keep all the advantages of using PresenceChannel

Disadvantages

  • A lot to code

So,

1. Create a new class which will implement Authenticable interface.

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use JsonSerializable;

/**
 * @property string $id
 * @property string $name
 */
class Session implements Authenticatable, Jsonable, Arrayable, JsonSerializable
{

    private $id;

    private $attributes = [];

    public function __construct($id)
    {
        $this->id = $id;
        $this->name = "Guest";
    }

    /**
     * Get the name of the unique identifier for the user.
     *
     * @return string
     */
    public function getAuthIdentifierName()
    {
        return 'id';
    }

    /**
     * Get the unique identifier for the user.
     *
     * @return mixed
     */
    public function getAuthIdentifier()
    {
        return $this->{$this->getAuthIdentifierName()};
    }

    /**
     * Get the password for the user.
     *
     * @return string
     */
    public function getAuthPassword()
    {
        return "";
    }

    /**
     * Get the token value for the "remember me" session.
     *
     * @return string
     */
    public function getRememberToken()
    {
        return $this->{$this->getAuthIdentifierName()};
    }

    /**
     * Set the token value for the "remember me" session.
     *
     * @param  string $value
     * @return void
     */
    public function setRememberToken($value)
    {
        $this->{$this->getRememberToken()} = $value;
    }

    /**
     * Get the column name for the "remember me" token.
     *
     * @return string
     */
    public function getRememberTokenName()
    {
        return "token";
    }

    public function __get($name)
    {
        return $this->attributes[$name];
    }

    public function __set($name, $value)
    {
        $this->attributes[$name] = $value;
    }

    /**
     * Convert the object to its JSON representation.
     *
     * @param  int $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this);
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        return $this->attributes;
    }

    /**
     * Specify data which should be serialized to JSON
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     * @since 5.4.0
     */
    public function jsonSerialize()
    {
        return $this->attributes;
    }
}

Modify this as you wish, but you shouldn't serialize $id property

2. Create UserProvider

<?php namespace App\Extensions;

use App\Models\Session;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;

class SessionUserProvider implements UserProvider
{

    private $store;

    /**
     * SessionUserProvider constructor.
     * @param Repository $store
     */
    public function __construct(Repository $store)
    {
        $this->store = $store;
    }


    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {
        return new Session(
            $this->getUniqueTokenForSession($identifier)
        );
    }

    /**
     * Retrieve a user by their unique identifier and "remember me" token.
     *
     * @param  mixed $identifier
     * @param  string $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByToken($identifier, $token)
    {
        return null;
    }

    /**
     * Update the "remember me" token for the given user in storage.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @param  string $token
     * @return void
     */
    public function updateRememberToken(Authenticatable $user, $token)
    {
        return;
    }

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        return null;
    }

    private function unpack($data)
    {
        return json_decode($data);
    }

    private function getUniqueTokenForSession($id)
    {
        return $this->retrieveCacheDataForSession($id)
            ->get('uuid');
    }

    private function retrieveCacheDataForSession($id)
    {
        $fluent = new Fluent(
            $this->unpack(
                $this->store->has($id) ? $this->store->get($id) : "[]"
            )
        );

        if(!$fluent->__isset('uuid')) {
            $fluent->__set('uuid', Str::random(128));
        }

        $this->store->put($id, $fluent->toJson(), 60 * 60 * 60);

        return $fluent;

    }

    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @param  array $credentials
     * @return bool
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        return null;
    }
}

Identifier property in retrieveById method is always session id if you are using broadcasting so you can also use this as a token.

3. Create new Guard

<?php namespace App\Services\Auth;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;

class GuestGuard implements Guard
{

    private $user;
    protected $request;
    protected $provider;

    /**
     * GuestGuard constructor.
     * @param UserProvider $provider
     * @param Request $request
     */
    public function __construct(UserProvider $provider, Request $request)
    {
        $this->provider = $provider;
        $this->request = $request;
    }


    /**
     * Determine if the current user is authenticated.
     *
     * @return bool
     */
    public function check()
    {
        return !is_null($this->user);
    }
    
    /**
     * Determine if the current user is a guest.
     *
     * @return bool
     */
    public function guest()
    {
        return !$this->check();
    }

    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        if($this->check()) {
            return $this->user;
        }

        $this->setUser(
            $this->provider->retrieveById(
                $this->request->session()->getId()
            )
        );

        return $this->user;
    }

    /**
     * Get the ID for the currently authenticated user.
     *
     * @return int|null
     */
    public function id()
    {
        return !is_null($this->user) ? $this->user->id : null;
    }

    /**
     * Validate a user's credentials.
     *
     * @param  array $credentials
     * @return bool
     */
    public function validate(array $credentials = [])
    {
        return false;
    }

    /**
     * Set the current user.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
     * @return void
     */
    public function setUser(Authenticatable $user)
    {
        $this->user = $user;
    }
}

Here in user method you pass session id as identifier, using broadcasting only this method is nessesary.

4. Register Guard and UserProvider in AuthServiceProvider.

// app/Providers/AuthServiceProvider.php

   /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('sessions', function (Application $app) {
            return new SessionUserProvider(
                $app->make('cache.store')
            );
        });

        Auth::extend('guest', function (Application $app, $name, array $config) {
            return new GuestGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
        });
    }

5.1 Add provider in config/auth.php

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],
        
        // New
        'sessions' => [
         'driver' => 'sessions',
         'model' => App\Models\Session::class,
        ],
    ],

5.2 Add guard in config/auth.php

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],

        // New
        'guest' => [
            'driver' => 'guest',
            'provider' => 'sessions'
        ]
    ],

6. Use your new guard

// routes/channels.php

Broadcast::channel('chat.{id}', function (Authenticatable $user){
    return $user;
}, ['guards' => ['guest']]);

Notice that you can use 'web' as a guard at the same time ('web' should be before 'guest'). It allows you to find out who is a guest and who is a logged in user - you can just check instance of Authenticable in channel callback.

And that how it looks in the laravel-echo-server database

Mover answered 22/6, 2019 at 8:6 Comment(0)
T
0

With the help of Renan Coelho i got it to work. The missing part for me was to override the Broadcast::routes() method with the following:

Route::post('/broadcasting/auth', function (Illuminate\Http\Request $req) {
    return Broadcast::auth($req);
});

Route::post('/broadcasting/auth'... is actually a route that gets added through the "Broadcast::routes()" method. This is why we override it here. You can see the active routes by typing php artisan route:list in your terminal.

Then, Renan Coelho already said, i had to add a custom Middleware (AuthenticateGuest) that creates a random user for me. (This is the hacky part) and add it to the $middleware array in the kernel.php:

protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
        \Barryvdh\Cors\HandleCors::class,

        \App\Http\Middleware\AuthenticateGuest::class
    ];

The AuthenticateGuest Middleware looks like the following:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use App\User;

class AuthenticateGuest
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        Auth::login(factory(User::class)->make([
            'id' => (int)str_replace('.', '', microtime(true))
        ]));

        return $next($request);
    }
}

Hope that helps someone,

Sebastian

Tarpeia answered 24/9, 2018 at 9:27 Comment(0)
S
0

My solution to issue:

BroadcastServiceProvider.php (~/app/Providers/)

public function boot()
{
    if (request()->hasHeader('V-Auth')) { /* Virtual client. */
        Broadcast::routes(['middleware' => 'client_chat.broadcast.auth']);
    } else {
        Broadcast::routes();
    }

    require base_path('routes/channels.php');
}

Kernel.php (~/app/Http/)

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    ...
    'client_chat.broadcast.auth' => \App\Http\Middleware\ClientChatBroadcasting::class,
];

ClientChatBroadcasting.php (~/app/Http/Middleware/)

public function handle($request, Closure $next)
{
    if (/** your condition **/) {
        $fakeUser = new User;
        $fakeUser->virtual_client = true;
        $fakeUser->id = /** whatever you want **/;
        $fakeUser->name = '[virtual_client]';
        $fakeUser->asdasdasdasdasd = 'asdasdasdasdasd';

        $request->merge(['user' => $fakeUser]);
        $request->setUserResolver(function () use ($fakeUser) {
            return $fakeUser;
        });
    }

    return $next($request);
}

ChatChannel.php (~/app/Broadcasting/Chat/)

Broadcast::channel('chat.{chatId}', ChatChannel::class); Channel Classes

public function join($member/**($fakeUser)**/, $chatId)
{
    $memberData = [/** your data **/];

    /* If there is no member data (null), then there will be an authentication error. */
    return $memberData;
}

[place in your js file, where you want connect to broadcasting]

this.Echo = new Echo({
        broadcaster: 'socket.io',
        host: /** your host **/,
        reconnectionAttempts: 60,
        encrypted: true,
        auth: {
            headers: {
                'V-Auth': true,
                'Access-Token': accessToken,
                'Virtual-Id': virtualId,
                'Chat-Id': chatId
            }
        }
    });
Stowe answered 30/7, 2019 at 10:32 Comment(0)
Q
0

I made it here reading this blog, getting the /broadcasting/auth error. The answers were very helpful but I didn't find the complete answer I needed.

I did this in Laravel 9.x so FYI it will work in that version too.

The major missing piece of code I needed was in the routes/web.php:

use Illuminate\Support\Facades\Route;
Broadcast::routes([
    'middleware' => ['web', 'authenticate-guest'],
]);

Route::get('/', function () {
    return view('welcome');
});

However I will add the rest down here in case anyone needs the whole thing:

Remove comment in app.config provider:

App\Providers\BroadcastServiceProvider::class,

Make middleware: php artisan make:middleware AuthenticateGuest.php Then update it, in app/Http/Middleware

public function handle(Request $request, Closure $next)
    {
        Auth::login(User::factory()->make([
            'id' => (int) str_replace('.', '', microtime(true))
        ]));

        return $next($request);
    }

app/Http/Kernel.php File..

    protected $routeMiddleware = [
        ...
        'authenticate-guest' => \App\Http\Middleware\AuthenticateGuest::class
    ];

app/Providers/BroadcastServiceProvider.php file should be this, but check:

    public function boot()
    {
        Broadcast::routes();

        require base_path('routes/channels.php');
    }

Lastly, the routes/channel.php file:

use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('common_room', function ($user) {
    return true;
});

To finish everything off, do a npm run dev, followed by php artisan route:clear and maybe php artisan cache:clear/php artisan config:clear as well. Hope that helps!

Questionary answered 20/7, 2022 at 21:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.