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.
Frontend setup
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
})
}
Backend setup
/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 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.
Browser Network Tab
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
Browser Application Tab
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
Edit:
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
.
2
Answers
Laravel respond with a 401 error code because the auth:sanctum middleware stops the request as you are not sending the auth parameter (access token), please see the new line in auth -> headers below.
Then it should work or at least reach the controller and log ‘test’
Please note the above answer is correct for mobile+api apps but not for spa+api apps.
EDIT:
from the Laravel docs (sanctum#spa authentiction)
It seems like axios is not passing the XSRF-TOKEN in any of the request header you posted so i would modify as below:
Next i would inspect the http://localhost:8000/api/pusher/auth request to make sure the token is passed, if the problem still persists i would follow the steps in:
Laravel docs -> Sanctum -> Authorizing Private Broadcast Channels
I used the Laravel Echo client package recommended by the documentation to establish broadcasting communication.
The Laravel Documentation also covers the authentication methods required for channel listening.
Solution
Install the
laravel-echo
package as a development dependency alongside thepusher-js
package.Create the necessary
Echo
class on the client side at the desired location for establishing the connection.After this, by invoking the global
window.Echo
, you can connect to the channels.I don’t know your intentions, but I must point out that listening to the
channel
in your code will immediately cease once thethen()
function is executed. Therefore, it is advisable to define an externalchannel
variable and write the channel listening into it when thethen()
branch is executed.Extra