I'm trying to set up private-channel broadcasting with my vue 3 spa + laravel sanctum backend. The broadcasting works if used without authorization (non-private channels) but when I try to access a sanctum-protected route (required for private-channels), a 401 is returned to the client.
This happens with both the plain pusher-js library as well as laravel-echo. I've been trying to debug this for some time now without any progress.
Backend setup:
/routes/api.php:
Route::middleware('auth:sanctum')->group(function () {
Route::post('/broadcasting/auth', function (Request $request) {
// 401 prevents this code from running
\Log::info('hello');
response()->json('hello');
});
});
/app/Events/ChatMessageSent.php:
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ChatMessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $chatSessionId;
public $chatMessage;
public function __construct($chatSessionId, $chatMessage)
{
$this->chatSessionId = $chatSessionId;
$this->chatMessage = $chatMessage;
}
public function broadcastOn()
{
return new PrivateChannel('chat.'.$this->chatSessionId);
}
}
Used inside ChatController.php like so:
event(new ChatMessageSent($session->id, $message));
BroadcastServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
public function boot()
{
Broadcast::routes(['middleware' => ['api', 'auth:sanctum']]);
require base_path('routes/channels.php');
}
}
/routes/channels.php:
<?php
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
use App\Models\ChatSession;
Broadcast::channel('chat.{chatSessionId}', function ($user, $chatSessionId) {
$chatSession = ChatSession::find($chatSessionId);
return $chatSession ? $chatSession->hasParticipant($user) : false;
});
VerifyCsrfToken.php:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
protected $except = [
'/api/pusher/auth',
'/api/broadcasting/auth',
];
}
I've commented App\Providers\BroadcastServiceProvider::class in in /config/app.php.
Frontend setup:
login.vue:
const signIn = () => {
axios.get('/sanctum/csrf-cookie').then((res) => {
store.authToken = res.config.headers['X-XSRF-TOKEN']
console.log(store.authToken)
axios
.post('/login', form)
.then(() => {
store.auth = sessionStorage.auth = 1
store.signInModal = false
})
.catch((er) => {
state.errors = er.response.data.errors
})
})
}
returns the XSRF-Token:
eyJpdiI6Imp0dEc5ck9NU1BZQUlqbUlxYVRoY3c9PSIsInZhbHVlIjoiSUJBNXNJVTg3OTFTN0c2VXluQzJ0Vk5sVGxnenU2aVhNWGE1V2tEMGFBNVBabmZDOFpqRWF6MFN0N0VLUDVRVmtXVFlmOTQ1MlcySFppWUM5eGZRcU1pSDlsN0grVCtRV2ZjYW1ONjFEdDQzT0JOUW4xalkwOGhaWGVXQi8rbHgiLCJtYWMiOiIyZjUyNGEzZTY5MmRiZDgyYzVhZDMyNGJhMjQ3NGFmYmE5ZDZhNDc0NDZiYTY3NTQ4MzFkYWZmMjU1YWNjNTkwIiwidGFnIjoiIn0=
chat.vue (call broadcasting-route):
import initEcho from '@/lib/laravel-echo.js'
const inquireChatSession = () => {
axios
.get(`/api/chat/${props.id}`)
.then((res) => {
state.chatSessionId = res.data.id
state.messages = res.data.messages
state.loadingSession = false
const echo = initEcho(store.authToken) // create laravel-echo instance with authToken, which tries to call authEndpoint (/api/broadcasting/auth) and authorize, throws 401
echo
.private(`private-chat.${state.chatSessionId}`)
.listen('ChatMessageSent', (data) => {
state.messages.push(data.chatMessage)
if (state.focusBrowserTab) {
state.messages.forEach((message) => {
if (message.sender_id !== store.user.id) {
message.read_status = 1
}
})
}
})
})
.catch((er) => {
state.errors = er.response?.data.errors
state.loadingSession = false
})
}
laravel-echo.js:
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
window.Pusher = Pusher
export default (token) => {
return (window.Echo = new Echo({
broadcaster: 'pusher',
key: 'bf29be46d8eb2ea8ccd4',
cluster: 'eu',
forceTLS: true,
wsPort: 443,
wssPort: 443,
enabledTransports: ['ws', 'wss'],
withCredentials: true,
authEndpoint: 'http://localhost:8000/api/broadcasting/auth',
auth: {
headers: {
'X-Requested-With': 'XMLHttpRequest',
Authorization: `Bearer ${token}`,
},
},
}))
}
axios.js:
import axios from 'axios'
axios.defaults.withCredentials = true
if (import.meta.env.DEV) {
axios.defaults.baseURL = 'http://localhost:8000'
}
I had to use a custom authorizor in my
laravel-echo.jsinit file:This made the
/api/broadcasting/authroute work.