Skip to content

Instantly share code, notes, and snippets.

@codewithgun
Created April 22, 2021 15:40
Show Gist options
  • Save codewithgun/140cfe807cfaf3cfbbeffbaeebf0add5 to your computer and use it in GitHub Desktop.
Save codewithgun/140cfe807cfaf3cfbbeffbaeebf0add5 to your computer and use it in GitHub Desktop.
Laravel web socket with local pusher and custom authentication

Laravel local websocket

Customized authentication will be used in this gist instead of default Auth facade provided by Laravel

Create project

composer create-project laravel-laravel your-project-name
cd your-project-name

Install all necessary libraries

composer require beyondcode/laravel-websockets
composer require pusher/pusher-php-server "~3.0"

The command above will install beyondcode/laravel-websockets, internally used php ratchet to handle web socket connection. Besides that, pusher/pusher-php-server is used for replacement of third party Pusher.

Setup WebSocket and Pusher

After the installation done, run the following command

php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

The command above will create configuration file for websocket at config/websockets.php. Since we are not using Pusher, we can put any value for PUSHER_APP_ID, PUSHER_APP_KEY and PUSHER_APP_SECRET.

PUSHER_APP_ID=AnyIdYouLike
PUSHER_APP_KEY=AnyKeyYouLike
PUSHER_APP_SECRET=AnySecretYouLike

Next, we need to use Pusher as your broadcasting driver. It can be done by setting BROADCASTING_DRIVER varaible in your .env

BROADCAST_DRIVER=pusher

After that, we are going to change the default behavior of Laravel broadcasting events which is to send the event to official Pusher server to our local Pusher API.

Open config/broadcasting.php and ensure that the pusher value is like below:

'pusher' => [
    'driver' => 'pusher',
    'key' => env('PUSHER_APP_KEY'),
    'secret' => env('PUSHER_APP_SECRET'),
    'app_id' => env('PUSHER_APP_ID'),
    'options' => [
        'cluster' => env('PUSHER_APP_CLUSTER'),
        'encrypted' => true,
        'host' => '127.0.0.1',
        'port' => 6001,
        'schema' => 'http'
    ],
],

We are removing 'useTLS' => true because we are using http instead of https.

Laravel Pusher replacement able to support multiple WebSocket application with one server. For more information, please visit https://beyondco.de/docs/laravel-websockets/basic-usage/pusher#configuring-websocket-apps

Run php artisan serve and php artisan websocket:serve. Then, open up your favorite browser such as Firefox and navigate to http://localhost:8000/laravel-websockets. If you get a WebSockets Dashboard, it means you have successfully setup the WebSocket and Pusher.

Setup Client

Open up your terminal and run the following command

npm install --save-dev laravel-echo pusher-js
npm run build

After that, open resources/js/bootstrap.js and add the follwing code

import Echo from 'laravel-echo';

window.Pusher = require('pusher-js');

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    forceTLS: false,
    wsHost: window.location.hostname,
    wsPort: 6001,
    disableStats: true,
});

Then, run npm run dev. This will use webpack to bundle the code in resources/js/bootstrap.js and create bundled js file in public/js/app.js

For now, we have finished all the basic setup. Let's start with the code.

php artisan make:event MessageEvent

Open your terminal and run the above command to generate an event for broadcasting.

The MessageEvent.php file generated at app/Events. When the event broadcasted, laravel will automatically convert all the public variable of the class into json.

By default, the generated MessageEvent class will broadcast on PrivateChannel which require authorization channel. For testing purpose, let change it to PublicChannel, which can be connected by anyone publicly.

public function broadcastOn()
{
    return new Channel('channel-name');
}

Now, let's add a message variable for representing the message to be broadcasted.

public $message;

public function __construct($message)
{
    $this->message = $message;
}

Then, the MessageEvent class must implements ShouldBroadcast interface.

class MessageEvent implements ShouldBroadcast

Finally, the MessageEvent.php will be the same as below.

class MessageEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    public function __construct($message)
    {
    $this->message = $message;
    }

    public function broadcastOn()
    {
        return new Channel('channel-name');
    }
}

We have done with the message broadcasting, Let's create an endpoint for client to send message and broadcast to channel channel-name

Run the following command to generate a controller.

php artisan make:controller MessageController

Add the following code to the MessageController newly created.

class MessageController extends Controller
{
    public function send(Request $request)
    {
        broadcast(new MessageEvent($request->message));
        return response()->json([], 200);
    }
}

The code above create a function, which the request will be routed to send function and the sent message will be broadcasted to the channel-name channel to be received by client.

Now, let's register a route for sending message.

Open routes/api.php and add the following route to it.

Route::post('/send', 'App\Http\Controllers\MessageController@send');

We have done all the backend implementation, let's move to client implementation.

Open resources/js/bootstrap.js and add the following code to it.

window.Echo.channel('channel-name').listen('MessageEvent', e => {
    console.log(e.message);
});

The code above allow client to subscribe to chat-channel and listen on message broadcasted by MessageEvent. Please make sure that the listen function parameter matched with your event class name.

After that, open your terminal and run npm run dev to rebundle your javascript file. We have done both client and server implementation. Let's move on to testing.

Since this gist is about creating local websocket without the official Pusher, this proof of concept will not any involve user interface development (with console in browser only) and security practices.

Open welcome.blade.php, which located at resources/views and add the following script to the end of the <body> tag.

<script src='js/app.js'></script>

The code above is to instruct the browser to load the app.js bundled by npm run dev to the page.

Now is time for testing. Run the following command in the terminal to start the laravel server and laravel websocket server.

php artisan serve
php artisan websocket:serve

Open any 2 browser, and navigate to localhost:8000. Once you saw the landing page of the laravel application, press F12 on your keyboard to open up browser console. Then, copy paste the following code to the console of one of the opened browser.

fetch("api/send", {
  body: JSON.stringify({message: 'Hello from laravel local pusher'}),
  headers: {
    Accept: "application/json",
    "Content-Type": "application/json"
  },
  method: "POST"
})

Once you execute the code above, you will get Hello from laravel local pusher output at another browser. Until now, we have successfully setup local pusher for laravel websocket using public channel. But what about private channel ?

For private channel that use default Auth facade of laravel for authentication, what you need to do is run php artisan migrate to create User table. After that, change the return type of broadcastOn function in MessageEvent class to a private channel.

public function broadcastOn()
{
    return new PrivateChannel('channel-name');
}

Please take note that in production environment, there will be multiple private channel instead of hardcoded channel-name in this gist.

Then, update the resources/bootstrap.js to listen on private channel instead of the public channel.

window.Echo.private('channel-name').listen('MessageEvent', e => console.log(e));

This allow you to have a private channel. User must login from laravel application in order to make the private channel work.

However, what if we didn't wish to use the default Auth facade for channel authentication?

Open app/providers/BroadcastServiceProvider.php and update the boot function to the following.

public function boot()
{
    Broadcast::routes(['middleware' => ['custom.auth']]);
    require base_path('routes/channels.php');
}

The code above will allow us to implement custom authentication in our own middleware. Now run the following command to generate the custom authentication middleware.

php artisan make:middleware CustomAuthentication

Now, let's register the middleware to the application. Open app/Http/Kernel.php and add your middleware into $routeMiddleware.

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
    'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    'custom.auth' => \App\Http\Middleware\CustomAuthentication::class
];

Open up app/Http/Middleware/CustomAuthentication.php and add the following code.

public function handle(Request $request, Closure $next)
{
    //Mocking authenticated user list without database
    $authenticatedUsers = array([
        'codewithgun' => 'gunwithpassword'
    ]);

    $authenticatedUsers = array(
        'codewithgun' => 'gunwithcode'
    );

    $user = $request->header('user');
    $password = $request->header('password');
    if (!array_key_exists($user, $authenticatedUsers)) {
        return response('Unauthorized', 401);
    }
    if ($authenticatedUsers[$user] != $password) {
        return response('Unauthorized', 401);
    }

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

    return $next($request);
}

The code above just mocking some users data for testing purpose. You should code you custom authentication logic here.

After that, open routes/channels.php and add the following channel to it.

Broadcast::channel('channel-name', function ($user) {
    return true;
});

The $user received here is the authenticated user, which we set into $request using $request->setUserResolver. If user was not set to $request, laravel will block the user. We simply return true to authorize the authenticated user to listen on this channel.

Then, open resources/bootstrap.js and add some authentication header to laravel echo.

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    forceTLS: false,
    wsHost: window.location.hostname,
    wsPort: 6001,
    disableStats: true,
    auth: {
        headers: {
            user: 'codewithgun',
            password: 'gunwithcode'
        }
    }
});

window.Echo.private('channel-name').listen('MessageEvent', e => {
    console.log("Message", e.message);
});

The code aboce set user codewithgun with password gunwithcode for authentication purpose. Besides that, we also changed laravel echo to listen on private channel instead of the public channel by changing window.Echo.channel to window.Echo.private.

Remember to run npm run dev to rebundle the bootstrap.js

Lastly, by default laravel application commented out application service provider for BroadcastServiceProvider.php, open config/app.php and uncomment the following code.

App\Providers\BroadcastServiceProvider::class,

Now everything is done, run php artisan serve and php artisan websocket:serve to test on it.

Please take note that there was no security consideration was taken, and most of the thing was hardcoded. However, it should be enough to serve as the basis for customization.

@AdeWang0629
Copy link

This material was really helpful to me.
Please continue to write good articles in the future.

@vipinganganiya
Copy link

vipinganganiya commented Jan 20, 2024

This blog really helped me, thanks.

@ANDRIANANTENAINA
Copy link

it's possible to turn it local ?

@sbaum2s
Copy link

sbaum2s commented Jun 5, 2024

You did not specify PUSHER_APP_CLUSTER. I doubt the code runs without it. Especially if you would set encrypted: true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment