In the app I'm currently building, I'm tinkering with Echo and Pusher.
I have an admin dashboard that listens for events on a private admin
channel in order to display real-time notifications.
The problem I'm having is that the /broadcasting/auth
route is erroring and returning a 403: forbidden status code.
Upon further investigation, I discovered that in the Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
class on lines 38 - 42, we have this check:
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()
) {
throw new HttpException(403);
}
So it's throwing an exception if the user is not logged in. Problem is in my case, the user _is_ logged in, but I'm using a custom auth guard. So in my case, this:
! $request->user()
...needs to be this:
! $request->user('admin')
I don't see a way this can be accomplished, unless I'm missing something? And the docs don't mention auth guards in the broadcasting section at all. Any advice is welcome!
For anyone else experiencing this, I've come up with a temporary solution. I've overridden the Illuminate\Broadcasting\BroadcastController
and implemented my own authenticate()
method, and then used the IoC container to override Laravel's BroadcastController
with my own. Seems a bit hacky, but it works for now.
You can pass route parameters:
Broadcast::routes(['middleware' => 'auth:admin']);
Hi @themsaid, that doesn't seem to work. Isn't the middleware bypassed by the ! $request->user()
check in the auth()
method?
The middleware will set the default guard to the one specified, which should make the check pass.
Hmmm, doesn't seem to be working? Copied your exact code, and am still getting the forbidden error on the route.
Broadcast::routes(['middleware' => 'auth:admin']);
How do you call the route?
The Pusher JS library does that, does it not? The endpoint is specified here: https://github.com/laravel/echo/blob/a47888655a66af72d91ebda949a19f4551222294/src/connector/connector.ts#L14
I'm not calling the route myself - but when Pusher tries to initialise itself, I'm getting a network error in my console - 403 forbidden error on the /broadcasting/auth
endpoint.
Are your routes cached?
Nope. Even ran php artisan cache:clear
to make sure that wasn't the issue.
Is this admin
guard using sessions? cookies? How does it work?
Pusher sdk makes a normal request to your app, if the auth guard is stateless you'll need to configure pusher to alter the request being made for it to work.
Not really sure what you mean by how does it work? It's just a regular auth guard to allow me to admins
as well as users
. They are separate models, and separate tables.
Here is my auth config:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
...
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Admin\Admin::class
]
]
I log the user in with Auth::guard('admin')->attempt($data);
and use $request->user('admin')
to access the admin object whenever I need it.
https://laravel.com/docs/5.3/authentication#adding-custom-guards
@taylorotwell Are you able to shed some light on this?
@viacreativedev I think you have conflicting session names causing the check to fail, I wonder why you want two session based guards in the same app, this may lead to having two users logged in at the same time.
Please move the conversation to the forums, I'm sure somebody will be able to help you there, we try to keep this repo for bug reporting only, at first you weren't sharing enough info about the issue that I thought it was a bug, but then I was able to auth private channels using a session guard without any problems, but getting more info from you I see that you might have session name conflicts where you authenticate users using a session name and checking for them using another session name.
You need to share more code about how you authenticate users using the other session guard? How does the login form look like and how do you authenticate in the controller, this will show more details about the conflict your'e having, also logged in as admin try to make a test GET route that uses auth:admin
middleware and hit it using your browser, if authorization failed you need to look into the session names and make sure the guard is generating the same name that the middleware is checking.
Hi @themsaid, there are two user types in my app: Admin and User. They both have completely different data structures and models, so I'm using auth guards to split them up.
I'm confused how to specifying the auth:admin
middleware in the routes will change the $request->user()
check in the Broadcaster classes? That method call isn't being given a parameter, so surely it will ALWAYS check the web
auth guard by default, and stipulating a middleware won't change that?
This line will switch the current guard, causing all future checks to use this guard as if it's the default one.
Ah, that makes more sense thanks. Only, I called dd($guards)
in that function to see which guards were being checked, and the admin
guard is the only one there:
Will keep looking.
yes when you do auth:admin
only the admin guard will be checked, that's normal.
Yeah, but it's the web
middleware that is being checked on the broadcasting/auth
endpoint
You're using this Broadcast::routes(['middleware' => 'auth:admin']);
right? that line would switch to using the admin guard.
Yeah I'm using that, but it doesn't seem to be using the admin middleware for the /broadcasting/auth
endpoint. It works everywhere else – if I create /test
route with the auth:admin
middleware, it works as expected.
Ok, found the issue. The route wasn't inside the web
middleware. This wasn't clear because the Broadcast::routes()
call was already there by default, but commented out in the Broadcasting service provider. It may be worth clarifying that in the docs.
Broadcast::routes(['middleware' => ['web','auth:admin']]);
Thanks for your help @themsaid.
Glad you finally found the issue :) and yeah it's a bit confusing and needs some clarification
How does this work when you have two guards that both use broadcasting? I can't seem to load the routes conditionally and the broadcasting/auth route seems like it's hard coded into Echo.
Any work around a?
Ok for anyone else wondering about this you can pass the authEndpoint
into the Echo constructor when using pusher.
new Echo({
broadcaster: 'pusher',
...
authEndpoint: '/guard/auth/broadcasting'
});
Then remove Broadcast:routes()
in the service provider and create a new route for each guard
$router->post('/guard/auth/broadcasting', function(Request $req) {
Broadcast::auth($req);
});
@ryantbrown I can not make it to work
cause the client still sends to the default auth endpoint regardless of authEndpoint passed to the constructor. It always send request to /broadcasting/auth
I found out after I did logging inside Exception
@ryantbrown thanks for your post. I believe that you should return Broadcast::auth($req), so it should look like:
$router->post('/guard/auth/broadcasting',function(Request $req){
return Broadcast:auth($req);
});
pusher-js expects a JSON response of the following:
{"auth" : "xxxxxx"}
It seems this might be the best way to implement custom guards.
In your Channels.php or BroadcastServiceProvider.php, make sure your Broadcast Channel
does NOT use a wildcard like {id}, but a star: '*'
Example:
Broadcast::channel('App.User.*', function ($user, $id) {
return (int) $user->id === (int) $id;
});
$router->post('/guard/auth/broadcasting', function(Request $req) {
Broadcast::auth($req);
});
How do I change request guard after this? verifyUserCanAccessChannel
in Broadcaster.php
still picks up a custom guard.
@ryantbrown when using your solution it gives $router is undefined variable in laravel 5.5
how can i define it?
@viacreativedev Could you tell us what you did exactly. we are currently having the same issue.
@julesbloemen I'm so sorry but this was over a year ago and I don't remember what I did - the project never made it into production.
However, it looks like further up I resolved it by defining my Echo routes like so. Does this not work for you?
Broadcast::routes(['middleware' => ['web','auth:admin']]);
@viacreativedev Thanks for your quick response! I will try to debug this a bit further.
@julesbloemen No problem at all. If you bump into any issues, feel free to drop me an email and I'll help out where I can!
terry[at]viacreative.co.uk
I had something similar happen, and after much frustration managed to resolve it.
I submitted a patch upstream and am hoping it gets accepted.
An extremely frustrating bug to resolve :) Go ahead and try the linked change out to see if it works for you.
for multiple guard, Simply provide multiple guards to the auth middleware so it will know which guard using for authentication:
Broadcast::routes(['middleware' => ['web', 'auth:admin, master']]);
@themsaid @sbkl Tell me if I'm wrong but there is something misleading here.
If we use this code :
Broadcast::routes(['middleware' => ['web', 'auth:admin,master']]);
We are actually enforcing the authentication for this route and not only changing the userResolver. By default, there are no guards for this route.
I want to remind that web
is the default group of middleware which is not enforcing authentication at all. It's different from auth:web
.
The broadcaster classes (in RedisBroadcaster@auth and PusherBroadcaster@auth) are calling $request->user()
which call the default userResolver for the request which is the userResolver of the default guard.
So what we really want is allowing to specify guards here no ?
I'm working on the issue https://github.com/laravel/framework/issues/23353 and I think it's related.
This works for me
i added a second channel in routes>channels.php
`Broadcast::channel('App.User.{id}', function ($user, $id) {//channel for middleware web
return (int) $user->id === (int) $id;
});
Broadcast::channel('App.Models.Auxiliar.{id}', function ($user, $id) {//middleware my custom guard aux
return (int) $user->id === (int) $id;
});
`
Soultion to Common Problems faced by many users while setting up Laravel Echo
https://gist.github.com/rauschmerscen/2f2265976d59267f3bfccb339b27be44
I finally got this to work with 2 separate login screens and 2 separate users and customers tables.
Firstly I followed the Laracasts video on private channel broadcasting. The video says to put all the echo event listeners in your bootstrap.js. This will work with one users table. However for 2 separate tables users, customers you need to place the relevant echo event listeners in your 2 separate app.blade.php layouts files. For users in one and for customers in the other. However the listeners should be positioned at the bottom.
window.Echo.private('App.User.' + window.Laravel.user.id)
.listen('Event', e => { etc. });
window.Laravel = {!! json_encode([ 'customer' => auth()->guard('customer')->user()
]) !!};
window.Echo.private('App.Customer.' + window.Laravel.customer.id)
.listen('Event', e => { etc. });
Then in your routes/channels.php
Broadcast::channel('App.User.{id}', function ($user, $id)
return (int) $user->id === (int) $id;
});
Broadcast::channel('App.Customer.{id}', function ($user, $id)
return (int) auth()->guard('customer')->user()->id === (int) $id;
}); // Note I do not compare "$user" here
Then in BroadcastServiceProvider.php
Broadcast::routes(['middleware' => 'web', 'auth:customer']);
require base_path('routes/channels.php');
//Remove Broadcast::routes();
A customer can receive a private message when an event occurs and so can a user. Hope this helps.
This is so painful issue. Im trying to figure this out for hours.
Most helpful comment
Ok, found the issue. The route wasn't inside the
web
middleware. This wasn't clear because theBroadcast::routes()
call was already there by default, but commented out in the Broadcasting service provider. It may be worth clarifying that in the docs.Thanks for your help @themsaid.