Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Latest Update broke auth #408

Open
robgordon89 opened this issue Jan 14, 2025 · 22 comments · May be fixed by #410
Open

Latest Update broke auth #408

robgordon89 opened this issue Jan 14, 2025 · 22 comments · May be fixed by #410

Comments

@robgordon89
Copy link

Echo Version

1.18.0

Laravel Version

11

PHP Version

8.3

NPM Version

8.19.4

Database Driver & Version

No response

Description

The latest release 1.18.0 broke channel auth please see attached, reverting back to 1.17.1 resolved it, I am not sure what the issue is i didnt have time to dig into it 👍

Image

Thanks
Bob

Steps To Reproduce

Install 1.18.0

@crynobone
Copy link
Member

Hey there, thanks for reporting this issue.

We'll need more info and/or code to debug this further. Can you please create a repository with the command below, commit the code that reproduces the issue as one separate commit on the main/master branch and share the repository here?

Please make sure that you have the latest version of the Laravel installer in order to run this command. Please also make sure you have both Git & the GitHub CLI tool properly set up.

laravel new bug-report --github="--public"

Do not amend and create a separate commit with your custom changes. After you've posted the repository, we'll try to reproduce the issue.

Thanks!

@JanMisker
Copy link

I also noticed this, I employ a custom authorizer because I use sanctum API tokens.
I had in place an authorizer object on the options to Echo, as described in the docs to Laravel 10: https://laravel.com/docs/10.x/broadcasting#customizing-the-authorization-request

But this is no longer supported, it seems mainly because of changes in Pusher.

Instead of that I now use this, which seems to work:

new Echo({
       ...
      channelAuthorization: {
        endpoint: 'myCustomEndpoint',
        headers: { Authorization: `Bearer ${token}` },
      },
      ...
});

Maybe this helps @robgordon89 ?

This isn't really documented anywhere.

@mandatoryGithubUser
Copy link

mandatoryGithubUser commented Jan 15, 2025

How can I use a custom authorizer that would post with axios? I'm using Breeze Nextjs with Breeze API only (Laravel), Sanctum requires CSRF tokens and that'd be saved when the auth happens.

In echo: 1.17.1. I could do this

/// Axios (Default from Breeze Next)
import Axios from 'axios'

const axios = Axios.create({
    baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
    },
    withCredentials: true,
    withXSRFToken: true
})

export default axios
/// echo.ts 
        return new Echo({
            ...,
            authorizer: channel => {
                return {
                    authorize: (socketId, callback) => {
                        axios
                            .post(`/broadcasting/auth`, {
                                socket_id: socketId,
                                channel_name: channel.name,
                            })
                            .then(response => {
                                callback(null, response.data)
                            })
                            .catch(error => {
                                callback(error)
                            })
                    },
                }
            },
      })

But now, my broadcasting/auth url defaults to the frontend url, instead of the backend url I set. If I set channelAuthorization endpoint, then there's no longer a CSRF token that my axios had when logging in.

@JanMisker
Copy link

Check the source of connector.ts, specifically the setOptions function: https://github.com/laravel/echo/blob/fdf9f172cf34bb89aff9f04bef6594951246e4e0/src/connector/connector.ts#L40C15-L40C25
It seems you don't need to extract the CSRF token yourself, it will be handled automatically. If you want to add more headers you can use the channelAuthorization option.

Would be nice to have some proper documentation for this.

@filipescaglia
Copy link

I'm having the same problem. I was trying to authenticate using the 1.18.0 version, but everything i tried wasn't working. I downgraded it to 1.17.1 and everything works fine.
In version 1.18.0 I couldn't make the auth request send the cookies at all.
My frontend is a separate Vue SPA

@mgrinspan
Copy link

My application has authEndpoint: '/api/broadcasting/auth', but Echo is failing while attempting to reach /broadcasting/auth (without the /api prefix). This only started after the latest update.

Further, after moving from authEndpoint to channelAuthorization.endpoint, Echo fails to set the X-CSRF-TOKEN header unless I also provide headers: {}

@ooshhub
Copy link

ooshhub commented Jan 16, 2025

Here are some repro steps, may or may not be the same issue as everyone is facing, or at least related. It looks like the options are being malformed under some conditions. Error in FF is Uncaught TypeError: this.options.channelAuthorization.headers is undefined, it's slightly different in Chrome I think - specifies that it was trying to set the CSRF key on headers, instead. Same cause though.
Repro:
package.json

{
    "name": "dummy-website",
    "version": "0.1.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "dev": "vite"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "vue": "^3.5.12",
        "laravel-echo": "1.18.0",
    },
    "devDependencies": {
        "vite": "^5.4.10",
        "@vitejs/plugin-vue": "^5.1.4"
    }
}

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="some-key">
    <title>awesome dot com</title>
    <script type="module">
        import Echo from "laravel-echo";
        const pusherKey = 'some-other-key';
        const pusherCluster = 'some-cluster';
        const engineUrl = 'https://somewhere-awesome';
        const bearer = 'whatever';

        const echo = new Echo({
            broadcaster: 'pusher',
            key: pusherKey,
            cluster: pusherCluster,
            forceTLS: true,
            channelAuthorization: {
                endpoint: `${engineUrl}/broadcasting/auth`,
                params: {
                    bearer_token: bearer
                }
            }
        });
    </script>
</head>
<body>
    Best site evar
</body>
</html>

And npm run dev
Works in 1.17, breaks in 1.18.
Removing the csrf token from the <meta> tag in the head also prevents the error.

Something to do with fetching the csrf token from the meta tag, and the structure of the channelAuth options seems to mess with the _defaultOptions and knock the headers key off.
I haven't dug into it, fixing production sites took priority 👀

Also in this particular instance, if relevant - the CSRF token in the page isn't even the one we want. Our Vue app which uses echo is embedded in a bunch of sites, which may or may not have their own tokens in the <head>, we don't even want the token auto-fetched...

@imrodrigoalves
Copy link

imrodrigoalves commented Jan 17, 2025

Hello! In the project I am working with using Angular and a Laravel API it also breaks the auth requrest.

this.echo$ = new LaravelEcho({
      broadcaster: "pusher",
      key: environment.socket.key,
      wsHost: environment.socket.host,
      wsPort: environment.socket.port,
      wssPort: environment.socket.port,
      forceTLS: environment.socket.tls,
      cluster: environment.socket.cluster ?? 'eu',
      encrypted: true,
      disableStats: true,
      enabledTransports: ["ws", "wss"],
      authorizer: (channel: any, options: any) => {
        return {
          authorize: (socketId: string, callback) => {
            this.http.post(environment.apiUrl + "/broadcasting/auth", { // This call is not respected.
              socket_id: socketId,
              channel_name: channel.name,
            }).subscribe((response) => {
              if (!!response) {
                return callback(false, response);
              }
              return false;
            });
          },
        };
      },
    });

@crynobone
Copy link
Member

Hey there, thanks for reporting this issue.

We'll need more info and/or code to debug this further. Can you please create a repository with the command below, commit the code that reproduces the issue as one separate commit on the main/master branch and share the repository here?

Please make sure that you have the latest version of the Laravel installer in order to run this command. Please also make sure you have both Git & the GitHub CLI tool properly set up.

laravel new bug-report --github="--public"

Do not amend and create a separate commit with your custom changes. After you've posted the repository, we'll try to reproduce the issue.

Thanks!

@by-artemis
Copy link

But now, my broadcasting/auth url defaults to the frontend url, instead of the backend url I set. If I set channelAuthorization endpoint, then there's no longer a CSRF token that my axios had when logging in.

I thought I was the only one. I've been struggling for days with the auth part as well. Found out authorizer is no longer supported then used channelAuthorization instead and had the endpoint set to "http://localhost:8080/api/broadcasting/auth" because it uses the frontend URL if i just put "/api/broadcasting/auth"

  channelAuthorization: {
      endpoint: 'http://localhost:8080/api/broadcasting/auth',
      headersProvider: () => {
        if (session?.accessToken) {
          return { 'Authorization': `Bearer ${session?.accessToken}` };
        }
      }
    }

The issue is that whatever is in .listen() is never called. My console.log()s never showed in the browser console. I've already tried almost everything to debug this but I still can't find the solution.

@by-artemis
Copy link

I'm having the same problem. I was trying to authenticate using the 1.18.0 version, but everything i tried wasn't working. I downgraded it to 1.17.1 and everything works fine.

Hello @filipescaglia
Are you using authorizer or channelAuthorization in 1.17.1? I just wanted to compare with my configuration. I really can't make it work.

@filipescaglia
Copy link

I'm having the same problem. I was trying to authenticate using the 1.18.0 version, but everything i tried wasn't working. I downgraded it to 1.17.1 and everything works fine.

Hello @filipescaglia Are you using authorizer or channelAuthorization in 1.17.1? I just wanted to compare with my configuration. I really can't make it work.

In 1.17.1 i'm using the authorizer.

@nicobleiler
Copy link

Also broke our config updating from 1.17.1 to 1.18.0

import ApiService from "@/services/ApiService";
import Echo from "laravel-echo";
import Pusher from "pusher-js";

const initEchoConfig = () => {
  window.Pusher = Pusher;

  window.Echo = new Echo({
    broadcaster: "reverb",
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
    authorizer: (channel: any) => {
      return {
        authorize: (socketId: string, callback: any) => {
          ApiService.post(
            import.meta.env.VITE_APP_API_URL + "/broadcasting/auth",
            {
              socket_id: socketId,
              channel_name: channel.name,
            },
          )
            .then((response) => {
              callback(false, response.data);
            })
            .catch((error) => {
              console.debug("error: ", +error.data);
              callback(false, error);
            });
        },
      };
    },
  });
};

export default initEchoConfig;

@crynobone
Copy link
Member

Someone can either submit a PR to fix or send in the reproducing repository so the team can look for a possible fix.

@JanMisker
Copy link

It's a bit cumbersome to make a repository because besides the code it also involves setting up a Pusher compatible server.

Either way it's clear that the removal of the authorizer config option should have been marked as a breaking change.

IMHO this change should be reverted and a new version 2.0 should be introduced without the authorizer.

@crynobone
Copy link
Member

crynobone commented Jan 20, 2025

IMHO this change should be reverted and a new version 2.0 should be introduced without the authorizer.

Technically based on how we handle changes for Socialite and Scout, 3rd party breaking changes doesn't dictate major release. Again, you are free to submit a PR but in order for the team to verify this and submit one we need a reproducing repository.

@kirkbushell
Copy link

Yup, just updated and saw this issue as well - our auth using the custom authorizer no longer worked, and we spent all day trying to figure out wtf is going on...

This should have been in a 2.x release as it broke compatibility. At the very least, authorizer should have been deprecated, updated in the documentation and removed as part of a major version update.

Technically based on how we handle changes for Socialite and Scout, 3rd party breaking changes doesn't dictate major release. Again, you are free to submit a PR but in order for the team to verify this and submit one we need a reproducing repository.

If this is the case, you're breaking the semver standard, which everyone uses and depends on for their versioning of packages. It means that we should in fact only be allowing updates of patches, as the only way to dependably update this single package.

Image

I'd also highly recommend that the EchoOptions (which are pretty small), should be correctly typed, as that would help resolve these issues immediately for devs (ie. it would have highlighted that the option is no longer valid, as the authorizer key would be highlighted as being invalid. I'd be happy to add a PR for that.

It's very easy to reproduce - add an authorizer function, and note that it's never called.

It seems also that this change mostly breaks those of us who are using laravel-echo within an SPA, of which we are too.

@r0bdiabl0
Copy link

Just figured out what was going on just now as well. Experiencing this issue too after the update.

@r0bdiabl0
Copy link

r0bdiabl0 commented Jan 21, 2025

Here is what I'm finding. Echo is just a wrapper for Pusher. Pusher must have changed something involving the authorizer method some of us were using as seen in examples above and like this:

const echoInstance = new Echo({
  broadcaster: 'pusher',
  key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
  cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
  authorizer: (channel) => ({
    authorize: async (socketId, callback) => {
      try {
        const response = await axios.post('/broadcasting/auth', {
          socket_id: socketId,
          channel_name: channel.name,
        });
        callback(false, response.data);
      } catch (error) {
        console.error('Authorization error:', error);
        callback(true, error);
      }
    },
  }),
})

We were using authorizer so we could inject our global axios instances that automatically included headers for auth and xsrf tokens. With the latest changes, we can't use that method anymore as it doesn't work. You now have to add the headers and the endpoint in the configuration and let it use it's own process to fetch the authorization:

const echoInstance = new Echo({
          broadcaster: 'pusher',
          key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
          cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
          channelAuthorization: {
            endpoint: `${api.defaults.baseURL}/broadcasting/auth`,
            headers: {
              Authorization: `Bearer ${session?.accessToken}`,
              'X-XSRF-TOKEN':
                document.cookie.match(/XSRF-TOKEN=(.*?);/)?.[1] || '',
            },
          },
        });

I can't find how else to inject your own axios instance anywhere but the above works now.

@r0bdiabl0
Copy link

r0bdiabl0 commented Jan 21, 2025

I think I found the replacement for the deprecated authorizer in the pusher-js library. It's the customHandler option on channelAuthorization. For those of you who need to modify the api request, this is what I needed but you can see the similarities from the before (above) and after (below):

        const echoInstance = new Echo({
          broadcaster: 'pusher',
          key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
          cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
          channelAuthorization: {
            customHandler: async (payload, callback) => {
              const { socketId, channelName } = payload;
              try {
                const response = await axios.post(
                  '/broadcasting/auth',
                  new URLSearchParams({
                    socket_id: socketId,
                    channel_name: channelName,
                  }),
                );
                callback(null, response.data); // This may be different based on your implementation. I needed response.data.
              } catch (error) {
                console.error('Pusher channel auth error:', error);
                callback(error, null);
              }
            },
          },
        });

Someone should put in a PR to the Laravel Echo client documentation making this very clear on how to use your own axios or fetch instance with this configuration for Pusher.

@leexin
Copy link

leexin commented Jan 21, 2025

I think I found the replacement for the deprecated authorizer in the pusher-js library. It's the customHandler option on channelAuthorization. For those of you who need to modify the api request, this is what I needed but you can see the similarities from the before (above) and after (below):

    const echoInstance = new Echo({
      broadcaster: 'pusher',
      key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
      cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
      channelAuthorization: {
        customHandler: async (payload, callback) => {
          const { socketId, channelName } = payload;
          try {
            const response = await axios.post(
              '/broadcasting/auth',
              new URLSearchParams({
                socket_id: socketId,
                channel_name: channelName,
              }),
            );
            callback(null, response.data); // This may be different based on your implementation. I needed response.data.
          } catch (error) {
            console.error('Pusher channel auth error:', error);
            callback(error, null);
          }
        },
      },
    });

Someone should put in a PR to the Laravel Echo client documentation making this very clear on how to use your own axios or fetch instance with this configuration for Pusher.

Confirmed this works with the Laravel Echo v1.18 and confirmed that authorizer option no longer works in this version.

@kirkbushell
Copy link

Here is what I'm finding. Echo is just a wrapper for Pusher. Pusher must have changed something involving the authorizer method some of us were using as seen in examples above and like this:

const echoInstance = new Echo({
broadcaster: 'pusher',
key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
authorizer: (channel) => ({
authorize: async (socketId, callback) => {
try {
const response = await axios.post('/broadcasting/auth', {
socket_id: socketId,
channel_name: channel.name,
});
callback(false, response.data);
} catch (error) {
console.error('Authorization error:', error);
callback(true, error);
}
},
}),
})
We were using authorizer so we could inject our global axios instances that automatically included headers for auth and xsrf tokens. With the latest changes, we can't use that method anymore as it doesn't work. You now have to add the headers and the endpoint in the configuration and let it use it's own process to fetch the authorization:

const echoInstance = new Echo({
broadcaster: 'pusher',
key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
channelAuthorization: {
endpoint: ${api.defaults.baseURL}/broadcasting/auth,
headers: {
Authorization: Bearer ${session?.accessToken},
'X-XSRF-TOKEN':
document.cookie.match(/XSRF-TOKEN=(.*?);/)?.[1] || '',
},
},
});
I can't find how else to inject your own axios instance anywhere but the above works now.

This is a good find, but if Echo is exposing 3rd party APIs through their own, it's something we should probably wrap correctly, to protect against these sorts of things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet