pusher / pusher-js

Pusher Javascript library
http://pusher.com
MIT License
2.11k stars 374 forks source link

401 (Unauthorized) when subscribing to private-channel (Vue/Laravel) #789

Open Arturexe opened 11 months ago

Arturexe commented 11 months ago

Do you want to request a feature or report a bug? Question/Bug

What is the current behavior? 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

Any idea what the problem is with the pusher route or how I could further debug this? Thank you in advance

What is the expected behavior? Private channel subscription should work.

**Which versions of Pusher, and which browsers / OS are affected by this issue? Tested Pusher versions: newest, 8.3, 7.6 MacOS: Ventura 13.2.1 Browser: Chrome