How to access http headers in Phoenix socket?

I also saw that one while diving into the Phoenix Framework core, but it seems to me that is what calls the Socket implemented done by the developer, but I may be wrong…

It’s also super strange that the docs for Phoenix.Socket.Transport list an example of implementing EchoSocket, which imo is not a transport, but a custom socket. I can see why there would not be much more structure to the transports themselves – as I imagine them being quite different – but still naming that behaviour Transport when it’s about socket modules is quite a curious choice.

1 Like

I agree with you, that this is confusing, but maybe @chrismccord(sorry to disturb you) can shed some light on us?

This is a bad practice in terms of security, therefore this will be the very last approach I will try.

You’re aware that phoenix does this by default? Watch out judging without knowing the full context. If we’re talking about “bad practice in terms of security” you can find multiple sources about JWT being exactly that. Maybe you’re mixing up plain JWTs versus the OAuth dance?

As always in software, it depends. JWT in place of cookies? pretty bad. Any form of JWT within OAuth implementing it in your own app, tread with caution. JWT as a very short lived/one-off token to “upgrade” a session from website to socket? Pretty fine by me. This is my use case. It also depends a lot on what you try to secure/protect.

But a bit more back on-topic, how are your clients going to access the phoenix channels endpoint? Is that client library easily able to add those headers? AFAIK the standard phoenixjs client only supports query parameters and not adding an Authentication HTTP header?

If I understand you this means an HTTP redirect, therefore defeating the security that the Third Party integration is meant to bring to the API.

Not sure about your interpretation, I’m seeing a phoenix socket endpoint that’s protected in the idiomatic way (one-off phoenix tokens) which you can access with a longer lived JWT token from your third party by means of making a pretty standard HTTP request that ‘translates’ between that token and a one-off connection token. If the redirect does not work (it’s in the standard but does not mean every client library implements it correctly) your ‘human’ clients could also do that interchange themselves. But as already said, it depends on a lot of context.

I work in security for Mobile APIs, thus I was just stating what is considered as not good to do, but was not my intention to judge you, thus if it sounded like that, then I have to apologize to you.

np, not too much offended about it other than seeing that specific comment under mine without context about something that’s running in production can seem a bit bad for others. Where the context should be that I’m using them in a way as where they are invented for (one-off or short-lived).

I made a simple test/dummy app which does the redirect (exchanging a JWT for a phoenix token that’s valid for only setting up the socket connection) But directly encountered that certain websocket clients don’t follow the redirect during the handshake (while the RFC standard says otherwise). So that route is also a bit bumpy and depends a lot on which client(s) you’re going to use.

Based on the above, another option is to not do the redirect and only make a plain REST endpoint on which you can exchange the JWT via auth headers for a short-lived phoenix token (that last one is pretty trivial to implement, counting 2-3 lines in total). Then you have the ‘safe’ part of using it within the header of the REST call and continue with a much smaller scoped and short-lived token for only making the socket connection via query params. One of the advantages of this approach is (as already mentioned) that you can handle authentication errors better.

Also went a bit deeper into looking how hard it is to add (cowboy) middleware before the endpoint as you already encountered putting a plug in front does not work. But this is not trivial and also seems way to hacky to be a valid approach for what you’re trying to achieve.

Another completely different idea (also seen in the wild) is letting clients connect unauthenticated to your socket and let them authenticate with a separate message as the first action after the connection is established, otherwise you just disconnect them. But given the info you’ve given so far this doesn’t work because you use a third party authentication that (i guess?) makes the request to your socket endpoint?

2 Likes

I was not really expecting that you or anyone else to go and code to try to find the solution for me. I was just looking for pointers to get me in the right direction. Thanks for all your effort and dedication in trying to help me. I really appreciate it.

I now realize that I may have not been explicit enough when I use the term Third Party. So, I have used it from the point of view of the developer following the quickstart I am writing.

Also, once this quickstart is about API security for high profile and sensitive APIs (banks, fintechs, cars, healthcare, retail, etc.), the security must be tight and directly connected to the Third Party token, because hackers target this very heavily. It’s a constant cat and mouse game to stay ahead of them, because they leverage all they can to breach into this APIs, and this may involve chaining several attack vectors.

Regarding the endpoint being secured it needs to be the websocket, and in this API security context it must be secured directly by the token provided by the Third Party, therefore the solution you proposed is not viable in this scenario.

So, why do they allow to access X HTTP headers, the x_headers, and not allow for the standard HTTP headers, like the Authorization header?

I’d like to know the answer to this too.

I’ve just spent a bit of time dealing with getting basic connection info used in auditing (user agent, IP address, and the “X” headers to deal with X-Real-IP) working in a LiveView. I don’t understand how kinda/sorta providing the data matters. It’s just weird. I can get user_agent in a standalone fashion. All of the X-* headers are in their own bucket. That seems really arbitrary.

If I was dealing with a reverse proxy that implemented Forwarded instead of X-Forwarded-For I wouldn’t be able to get at the data.

Is there a specific reason for the selective exposure in this manner?

2 Likes

I don’t see coherence in the decision from looking at how the implementation works, but some rationale must exist that is not that obvious :thinking:

Maybe @josevalim or @chrismccord can explain us the rationale behind it?

1 Like

On my init_assigns.ex I want to avoid identifying the user if it’s a healthcheck bot. I can determine if it’s the bot easily using request headers but this isn’t possible with the socket exposed in the liveview.

    defmodule GamingWeb.InitAssigns do
      import Phoenix.LiveView
      
      def on_mount(:default, params, session, socket) do
        socket.headers?

        if user do
          Segment.Analytics.identify(user.id, %{email: Accounts.user_email(user)})
        else
          Segment.Analytics.identify(session["anonymous_user_id"])
        end

In the meantime I am trying to disable Render from making that GET / entirely - but it would be nice to be able to access the headers from within the liveview.

Sorry to necro an old thread here, but @Exadra37 did you ever reach a good alternative solution for this? I tried to do a similar approach to your rewrite-as-an-x-header-via-plug approach and came up with the same ineffective result.

I had to resort to use the x-header approach enforced by Phoenix, because the framework it’s inflexible in this regard.

The reason why this is not allowed is due to poor browser security. WebSockets are cross-domain, which means that, if you authenticate into a WebSocket with cookie/auth headers, an attacker on a completely different website can use the same credentials to open up and control a WebSocket with your credentials on any website.

Even if you plan to use WebSockets outside of the browser, in my humble opinion, just use the x-headers, as otherwise it is not worth introducing such a big hole in your app. If you want to risk it anyway, latest Plug (v1.14+) and Phoenix (v1.7+) allows you to upgrade any connection to a WebSocket, so that could be used instead. Here is an example: GitHub - elixir-plug/plug: Compose web applications with functions

About the part “otherwise you just disconnect them”, do you have any ideas on how to implement it? I’m assuming you meant “disconnect if the user doesn’t authenticate in X seconds”.

What I have tried

In the user socket’s connect/3

  1. Schedule an :auth_timeout message using Process.send_after/3 to self()
  2. Save the timer reference into the socket assigns
  3. Handle the :auth_timeout and disconnects the socket.

In the user channel’s join/3

  1. Cancel the timer once the user authenticates.

However, Phoenix.Socket brings its own handle_info(message, state) that matches everything. So instead of sending my custom :auth_timeout, I tried sending a %Broadcast{event: "disconnect"} that I see in the source code. However, it looks the process that runs the connect/3 is not the same that would be listening for these messages, so nothing happens.

Do you know which process would it be?

I’ve figured it out: it’s the Cowboy’s process :cowboy_clear.connection_process/4. You can get its PID from the socket’s connect/3 like this:

{:links, [pid]} = Process.info(self(), :links)

Then you create a timer to disconnect the socket using something like:

timer_ref = Process.send_after(pid, %Broadcast{event: "disconnect"}, 3000)

Save the timer_ref in the socket’s assigns and cancel it later in the Channel’s join, like:

Process.cancel_timer(socket.assigns.auth_timer)
1 Like