vipulbhj

vipulbhj

How to use Phoenix `socket` on localhost with React Frontend?

Hello, I am trying to build a Backend system in Phoenix, this is mostly APIs but needs Websockets for a chat interface.

I researched a bit with Claude, and found out that Phoenix sockets can be a great choice for this with Channels, so I create a setup as details below.

I have two tables

chat_sessions
chat_messages

When the user lands on /api/v1/chat/sessions, I check for a few things and then create a record in this table, and I use put_session to set the id of this table in a cookie and send it back to the client.

Now I created a socket endpoint in the endpoint file similar to /live just under /live

socket "/api/chat", ProfiWeb.UserSocket,
    websocket: [connect_info: [session: @session_options]],
    longpoll: false

Inside the UserSocket I added

def connect(params, socket, connect_info) do
    Logger.info("WebSocket connection attempt with params: #{inspect(params)}")
    Logger.info("Connect info session: #{inspect(connect_info[:session])}")
   
    with {:ok, session_id} <- extract_session_id(connect_info),
....
...


defp extract_session_id(connect_info) do
    case connect_info[:session] do
      %{"chat_session_id" => id} when is_binary(id) ->
        Logger.info("Found chat_session_id in session: #{id}")
        {:ok, id}

      _ ->
        Logger.info("No session found, checking params for session_id")
        {:error, :no_session_id}
    end
  end

Backend is running on http://localhost:4000 and FE is on http://localhost:4321

I was hoping this setup would be enough and I will get the cookie since inside session options, same_site is set to “Lax”

 @session_options [
    store: :cookie,
    key: "_profi_key",
    signing_salt: "4yRHL+9g",
    same_site: "Lax"
  ]

But when I check the request headers in browser, I don’t see the cookie, googling and going back and forth with Claude, finally landed on creating a reverse proxy on localhost:3000 and proxying both through that.

When I do that, browsers start sending the cookie, but the server still doesn’t get it.

I fought this problem a lot, with Claude a lot but nothing worked, added a ton of logs but nothing.

Does anyone know how I can do this ?

Thank you for your help, much appreciated :folded_hands:

#phoenix-forum:phoenix-questions

EDIT: If I missed any details, please let me know, I will update the post with those details.

First Post!

LostKobrakai

LostKobrakai

The cookie is not available for channels by default due to concerns around CSWSH vulnerabilities.

The session connect_info option has some further requirements. Are you sure you handled those:

In order to validate the session, the “_csrf_token” must be given as request parameter when connecting the socket with the value of URI.encode_www_form(Plug.CSRFProtection.get_csrf_token()).

Most Liked

Nicodemus

Nicodemus

First, you’ve chosen one of the most difficult ways to architect a website on the modern web, owing mostly to a gauntlet of security measures you will have to properly configure and work with. What is your app, and does it require a completely separate front-end? Does it even require React? 90% of SPAs are over-engineered, fragile, and worse off for being SPAs instead of progressively enhanced classic web sites, mainly due to an over-indulgence in following trends and cargo culting.

Are you going to host it on two separate domains, as is implied by running two dev servers. If you instead configure Phoenix to serve all of your static assets, then you don’t need to configure CORS, or deal with cookies being set for two different domains (though sharing across subdomains is relatively easy.) You can still share a domain with two different services behind it, but then you’ll need an HTTP aware proxy/load balancer to know that “/api” requests go to the Phoenix cluster, and “/app” requests go to the front-end nodejs or whatever cluster. So my main question is, what are you building, and does it really require this complexity? None of it is impossible, but if you’re new to these things (assumed by use of LLMs for research, and generally for the kinds of questions you’ve asked here and on Discord, apologies if you’re not), then you might want to reevaluate trying to build something so complex without some experience with simpler setups.

Second, Phoenix.Socket will likely never allow you to access arbitrary headers or cookies as LostKobrakai said, due to Cross-Site Websocket Hijacking. This is a serious problem, and I agree with Phoenix’s hard-line stance to basically make it close to impossible to fall into this trap. There is no way around this other than implementing your own version of Phoenix Channels.

I’m honestly not sure why using a local proxy made the browser start sending cookies for the socket connection, but I suspect it was coincidental. Websocket() will send any set cookies, depending on the cookie setting. However, fetch() will not allow the server to set cookies. So if your FE is truly separate and you’re “landing on /api/v1/chat/session” with fetch(), the only way the session cookie could have been sent was if you manually made a request to your BE on localhost from your browser at some point, which set the cookie. However, if you do deploy this to two separate domains, then it’s going to be very difficult to get the session cookie from one domain to the other, anyways.

If you really want to have a separate FE and BE, on separate domains, then the simplest solution is to treat the FE like it’s a completely different application running in a completely environment. Don’t use session data or cookies at all, and instead follow the guide that Kapsy in Discord already sent you.

The only difference, is that instead of setting the token in the assigns and in the JS of the page (which works if phoenix serves your FE), you will need to make a fetch() request from the FE to the BE with authentication information to your API, which will return a token for the JS in the FE to use when connecting to the Channel.

BTW, none of this is the fault of Phoenix or Elixir… this is all due to the security requirements of the modern web, which has had to add a lot of hoops to jump through due to bad actors exploiting vulnerabilities like CSWSH and CSRF to hack web sites. That and your desire to create one of the most difficult stacks to build a web site with, requiring knowledge and experience across a wide swatch of technologies, protocols, and security standards to get right.

Last Post!

vipulbhj

vipulbhj

I am really sorry, I didn’t have enough access for me to change the title of this post, once I had created it.

When I created this topic, I hadn’t fully understood what I was trying to do, what was the exact problem, or even what I was doing. But in the process of exploration, I learnt a lot and finally fixed my issue.

I want to reply to your message earlier but forgot, sorry about that as well.

What is your app, and does it require a completely separate front-end?

I was playing with that idea of, how do those chatbots on websites work. You know that annoying popup which makes this weird sound, I though those are external services that work on any platform, so how would I build one, if I had to.

Nothing to do with React, SPA, or anything else. I just care about their existing a web browser.

Are you going to host it on two separate domains, as is implied by running two dev servers.

Yeah, I am thinking, I provide a documentation page to people who want to connect with my service, which shows how to setup a proxy, so they can forward cookies and traffic to my service correct and we can establish a websocket connection, but also being able to track chat sessions.

fetch() will not allow the server to set cookies.
Actually it does, you just need to set credentials: include Using the Fetch API - Web APIs | MDN

BTW, none of this is the fault of Phoenix or Elixir… this is all due to the security requirements of the modern web, which has had to add a lot of hoops to jump through due to bad actors exploiting vulnerabilities like CSWSH and CSRF to hack web sites.

Absolutely agreed, I actually only figured it our, when I cloned Phoenix’s source locally and started tracking out things, documenting stuff, and finally understood the whole transporter pipeline, which, once I understood what was going on, made figuring out what was going on super easy.

The issue with what I was doing, was non of what I had suspected. It was just one of those things where, if you have not been burnt by it before, you just don’t pay enough attention about it.

My issue was, since I was setting cookies in the browser though an API call, in router.ex, api pipeline was

pipeline :api do
    plug :accepts, ["json"]

    plug Guardian.Plug.Pipeline,
      module: MyApp.Guardian,
      error_handler: MyApp.AuthErrorHandler

    plug Guardian.Plug.VerifyHeader, scheme: "Bearer"
    plug Guardian.Plug.LoadResource, allow_blank: true
  end

And I was using put_session to put cookies I wanted in connect_info to the browser, but it would always turn up blank, so after tracking through phoenix’s source code (which was extremely approachable btw, amazing work to all core devs), I realised a key piece of detail.

If you see pipeline :browser do in the router.ex file, you will find plug :protect_from_forgery, this one line does such a cool thing, if you see the implementation

def protect_from_forgery(conn, opts \\ []) do
    Plug.CSRFProtection.call(conn, Plug.CSRFProtection.init(opts))
  end

We are calling CSRFProtection.call, which if you check the implementation of def call(conn, {session_key, mode, allow_hosts}) do plug/lib/plug/csrf_protection.ex at 065976d85f3d079d4f22dde7897c2e7f67c596e0 · elixir-plug/plug · GitHub

You see the missing magic, with plug :protect_from_forgery, we get for free unmasked_token = Plug.CSRFProtection.dump_state(), aka the cookie automatically include put_session(:_csrf_token, unmasked_token)`, which is what is used to validate csrf_tokens.

Once, I realised what was wrong, the fix was obvious, and once I had done that I realised I had see this details already, phoenix/test/phoenix/socket/transport_test.exs at 1f969b5ed7584b61c8c1934108d865ed0ea65735 · phoenixframework/phoenix · GitHub this test is doing exactly this, but I hadn’t realised so, till I went on this journey.

The systems has been working great for me since, I learnt a lot about Phoenix in the process, and more about figuring thing out :smiley:

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement