I've set up a Vue3/Laravel
app with a live-chat via Pusher
which works over non-private channel chat
. In the next step I want to use a private channel but something weird happens. The pusher.subscribe
function that tries to send a request to /api/pusher/auth
doesn't seem to handle the sanctum authorization
correctly, resulting in:
POST http://localhost:8000/api/pusher/auth 401 (Unauthorized)
ajax @ pusher-js.js?v=1974b27b:676
(anonymous) @ pusher-js.js?v=1974b27b:3548
authorize @ pusher-js.js?v=1974b27b:1860
subscribe @ pusher-js.js?v=1974b27b:1828
subscribe @ pusher-js.js?v=1974b27b:3960
subscribeAll @ pusher-js.js?v=1974b27b:3951
(anonymous) @ pusher-js.js?v=1974b27b:3868
emit @ pusher-js.js?v=1974b27b:1230
updateState @ pusher-js.js?v=1974b27b:2341
connected @ pusher-js.js?v=1974b27b:2281
callback @ pusher-js.js?v=1974b27b:2176
cb @ pusher-js.js?v=1974b27b:2619
tryNextStrategy @ pusher-js.js?v=1974b27b:2459
(anonymous) @ pusher-js.js?v=1974b27b:2507
(anonymous) @ pusher-js.js?v=1974b27b:3399
finish @ pusher-js.js?v=1974b27b:1752
onMessage @ pusher-js.js?v=1974b27b:1729
emit @ pusher-js.js?v=1974b27b:1230
onMessage @ pusher-js.js?v=1974b27b:1327
socket.onmessage @ pusher-js.js?v=1974b27b:1343
Show 20 more frames
Show less
pusher-js.js?v=1974b27b:979 Pusher : : ["Error: Unable to retrieve auth string from channel-authorization endpoint - received status: 401 from http://localhost:8000/api/pusher/auth. Clients must be authorized to join private or presence channels. See: https://pusher.com/docs/channels/server_api/authorizing-users/"]
This problem is specific to the pusher
route, all other api routes work just fine.
pusher.js:
import Pusher from 'pusher-js'
Pusher.logToConsole = true
const pusher = new Pusher('bf29be46d8eb2ea8ccd4', {
cluster: 'eu',
forceTLS: true,
authEndpoint: 'http://localhost:8000/api/pusher/auth',
withCredentials: true,
wsPort: 443,
wssPort: 443,
enableStats: false,
enabledTransports: ['ws', 'wss'],
auth: {
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
}
})
export default pusher
axios.js:
import axios from 'axios'
axios.defaults.withCredentials = true
if (import.meta.env.DEV) {
axios.defaults.baseURL = 'http://localhost:8000'
}
the user is authenticated in a login component:
const signIn = () => {
axios.get('/sanctum/csrf-cookie').then(() => {
axios
.post('/login', form)
.then(() => {
store.auth = sessionStorage.auth = 1
store.signInModal = false
})
.catch((er) => {
state.errors = er.response.data.errors
})
})
}
and the app tries to subscribe to pusher in a chat component, only accessible to authenticated users:
const inquireChatSession = () => {
axios
.get(`/api/chat/${props.id}`)
.then((res) => {
state.chatSessionId = res.data.id
state.messages = res.data.messages
state.loadingSession = false
const channel = pusher.subscribe(`private-chat.${state.chatSessionId}`) // 401 happens here
channel.bind('App\\Events\\ChatMessageSent', (data) => {
state.messages.push(data.chatMessage)
})
})
.catch((er) => {
state.errors = er.response.data.errors
state.loadingSession = false
})
}
/routes/api.php:
Route::post('/pusher/auth', function (Request $request) {
\Log::info('test');
$user = $request->user();
if (!$user) {
abort(403, 'Unauthorized');
}
$pusher = new Pusher(
env('PUSHER_APP_KEY'),
env('PUSHER_APP_SECRET'),
env('PUSHER_APP_ID'),
['cluster' => env('PUSHER_APP_CLUSTER')]
);
$channelName = $request->channel_name;
$socketId = $request->socket_id;
$auth = $pusher->socket_auth($channelName, $socketId);
return response()->json(['auth' => $auth]);
})->middleware('auth:sanctum');
pusher is trying to connect to this route but the authorization fails, resulting in 'test'
not being logged and returning aforementioned error back to the client. This seems to be a vue spa + pusher + sanctum
problem because connecting to this diagnostic route works:
Route::post('/pusher/auth', function (Request $request) {
\Log::info(var_export($request, true));
\Log::info('Request headers: ', $request->header());
\Log::info('Request cookies: ', $request->cookies->all());
\Log::info('Session data: ', $request->session()->all());
\Log::info('User: ', $request->user());
})
but $request->cookies->all()
is empty and $request->user()
is null
. For some reason no auth cookies
are arriving in the pusher route. To check if sanctum
works by itself, connecting to the following route, returns the authorized user:
Route::middleware('auth:sanctum')->get('/test-auth', function (Request $request) {
return $request->user();
});
relevant .env entries:
APP_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhost
PUSHER_APP_ID=1728518
PUSHER_APP_KEY=bf29be46d8eb2ea8ccd4
PUSHER_APP_SECRET=...
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=eu
like I said, the entire authentication of the app works fine except the pusher route.
auth request #1:
General:
Request URL: http://localhost:8000/api/pusher/auth
Request Method: OPTIONS
Status Code: 204 No Content
Remote Address: 127.0.0.1:8000
Referrer Policy: strict-origin-when-cross-origin
Response Headers:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: x-requested-with
Access-Control-Allow-Methods: POST
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Max-Age: 0
Cache-Control: no-cache, private
Connection: close
Content-Type: text/html; charset=UTF-8
Date: Wed, 27 Dec 2023 02:25:13 GMT
Host: localhost:8000
Vary: Access-Control-Request-Method, Access-Control-Request-Headers
X-Powered-By: PHP/8.3.1
Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en;q=0.9,de;q=0.8
Access-Control-Request-Headers: x-requested-with
Access-Control-Request-Method: POST
Cache-Control: no-cache
Connection: keep-alive
Host: localhost:8000
Origin: http://localhost:3000
Pragma: no-cache
Referer: http://localhost:3000/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
followed by auth request #2:
General:
Request URL: http://localhost:8000/api/pusher/auth
Request Method: POST
Status Code: 401 Unauthorized
Remote Address: 127.0.0.1:8000
Referrer Policy: strict-origin-when-cross-origin
Response Headers:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:3000
Cache-Control: no-cache, private
Connection: close
Content-Type: application/json
Date: Wed, 27 Dec 2023 02:25:13 GMT
Host: localhost:8000
Set-Cookie: XSRF-TOKEN=eyJpdiI6Inl5T2ZLbndpZG1OUTV5MmxNdDlNNWc9PSIsInZhbHVlIjoiUzdJYVkzZzJvM3FnaUlIUGxVWFBDTTZYeHQveTBWOWoxSEsvcThGM00wVDh6WExmK2RYWVBldTNxK2xKS1RrV1JSTHA2b0NEMVFtQzlzSmxyVVVRbmlrSmNRdmJQaW00cWpIQVFyZkhYM0RwampuMDZWVzJsV3NUZjVJZ1kxaG0iLCJtYWMiOiI4MzFlZjBjYWZkNDZkZDBhMGYxZDgwMDQ5YTgzY2ExNDg1NDMyNjFlNTNmZDg5NGJmZTI4MDMxNzAzMjVlNjZjIiwidGFnIjoiIn0%3D; expires=Wed, 27 Dec 2023 04:25:13 GMT; Max-Age=7200; path=/; domain=localhost; samesite=lax
Set-Cookie: soul_meatcom_session=eyJpdiI6Ii8wTktnTVFMZUZBYXVTeTFTRjd4dmc9PSIsInZhbHVlIjoicDUzNjFJVEYyTVR5cXdrTGZQZWZ1NzF4UEZ6QUJXSWF3YUsya0lUZy9qb0IwNk0rM0cwa3RwV1YyZ1Q0T0JqWW90cjZKd2d3OXNqOW13aGswc2tPMGw0d0hPRkxDZDdqamFUQWpKSktVd2ZpS1c2b3NqQm5WMVhoK2VsLzJWeEkiLCJtYWMiOiI4YjM3ZWEwZWMwNzlhNDIxMWNhNjBhMjAzNzcxNDM0NGMxNTczOTU1YWQ0ZGFjNzEyYWJkNDI2ZWJiNjI2ZTZkIiwidGFnIjoiIn0%3D; expires=Wed, 27 Dec 2023 04:25:13 GMT; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax
X-Powered-By: PHP/8.3.1
Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en;q=0.9,de;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 87
Content-Type: application/x-www-form-urlencoded
Host: localhost:8000
Origin: http://localhost:3000
Pragma: no-cache
Referer: http://localhost:3000/
Sec-Ch-Ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
there are two cookies in /storage/cookies/http://localhost:3000:
#1 soul_meatcom_session:
eyJpdiI6ImI4dUNWL0pCRmRibFMzNUdyT0JrL3c9PSIsInZhbHVlIjoiQTN4L0djZm52bjlCSFV4TmM0QU1oNUFoT0JBUlFGcGNWMFVwSDEvTFZBWmZwVi9kSEFnUitHcit6MzRLNXVkNkxLU1o5a0VhWmJ2OTNvYUdxMkpyVDVUcVZoQWRzckVlVi84Tis3UTdxazhkR0ozU1EyeldnaFowcStTRFFJYjgiLCJtYWMiOiJkNjc3MGM1ODc2MWM1NWFiMDBlNjYzMTg0OWI3M2RiZmNmZGU5NzU4Y2QzZDA0NmViZDQzZjIzODBiMWZiYWM1IiwidGFnIjoiIn0%3D
#2 XSRF-TOKEN:
eyJpdiI6Ik1xc0tCckEzS2RNNURFVWJ5aGc2Z0E9PSIsInZhbHVlIjoidVE4TElScENjRFlEbUFtVk1sVzZ1MGU5WDI2NXk2b214aEpWbU10K1hJUGZtQzdFOFBHV3JYblZiYmlFSmQvaSt2ZWQ5cWtjOXhtZXJQTmQ0NUNSVjAvQ2xmVDNwcUw0dkRFMHZnclRSc08wanVqaHdlbGFWeE5JMk1pTzRXOFgiLCJtYWMiOiJkNzQwODAwYzU2ZGE0OTVjNzQ0MjQxNzAwZDIxMGVkNGNkZTJjNWI2NjQ2YjMzZjk4NGM1YzI4MWJhOWZmMGI2IiwidGFnIjoiIn0%3D
I'm not very experienced in reading network headers. Any idea what the problem is with the pusher
route or how I could further debug this? Thank you in advance
I've tried @suxgri's approach:
import Pusher from 'pusher-js'
Pusher.logToConsole = true
const pusher = new Pusher('bf29be46d8eb2ea8ccd4', {
cluster: 'eu',
forceTLS: true,
authEndpoint: 'http://localhost:8000/api/pusher/auth',
withCredentials: true,
wsPort: 443,
wssPort: 443,
enableStats: false,
enabledTransports: ['ws', 'wss'],
auth: {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Authorization':
'Bearer eyJpdiI6ImpCdC96YjVqNmh3VXhic0tkeXlYQ3c9PSIsInZhbHVlIjoiN0YzMzB3QkVySDdyeFdIK0JLaXM1cWVaTDZSd3FTUkNoWHdDSEFVaTQ3ZktYMi94ak5yQVpMRkNNNjBNbWswamM0emJGa2RVRktuNU4yUXBLMUxoMU90UGxyZk94bzdUSythMlFFNmNGdFlydjdhYVI4WmlIRXk4dEdKQU9kRnYiLCJtYWMiOiIwOWI3MDg4MGZmYTAzYWY3N2QzOGM5ZmQ4MjNkMjIyNDg5OGRjZTk5YjNjNTAwZjE5MWY2YjIxZTMyMGQ3NWU0IiwidGFnIjoiIn0=',
},
},
})
export default pusher
but I still get the 401
.
In the meantime I tried out laravel-echo
only to find the exact same problem. I still don't know why pusher
and sanctum
authorization won't work by default but modifying my laravel-echo.js
to use a custom authorizor, made the /api/broadcasting/auth
route work:
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,
withCredentials: true,
authEndpoint: 'http://localhost:8000/api/broadcasting/auth',
auth: {
headers: {
'X-Requested-With': 'XMLHttpRequest',
Authorization: `Bearer ${token}`,
},
},
authorizer: (channel, options) => {
return {
authorize: (socketId, callback) => {
axios
.post('/api/broadcasting/auth', {
socket_id: socketId,
channel_name: channel.name,
})
.then((response) => {
callback(null, response.data)
})
.catch((error) => {
callback(error)
})
},
}
},
}))
}
For plain pusher-js
it is probably /api/pusher/auth
route, that has to be configured (if pusher-js supports custom authorizor at all).