vipulbhj
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 ![]()
#phoenix-forum:phoenix-questions
EDIT: If I missed any details, please let me know, I will update the post with those details.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixir-ls
- #elixirconf-us
- #ai
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming











Showing Posts 1 to 7- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
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:
vipulbhj
Thank you for sharing that, I read it, but not sure what I missed, is their an example I can maybe look at ?
Sorry if I missed something obvious.
I tried adding
token: Plug.CSRFProtection.get_csrf_token()to the/api/v1/chat/sessionsresponse, and thenI even added
check_origin: falsetoStill doesn’t work.
Proxy (
Caddyfile)vipulbhj
@josevalim can you help with this ?
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” withfetch(), 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.
LostKobrakai
@vipulbhj GitHub - LostKobrakai/channel_session · GitHub this got a working example. I guess you didn’t pull in the connect_info in the connect callback of the socket. Stuff one doesn’t really think of when not having a concrete codebase in front of their nose.
vipulbhj
@LostKobrakai Thank you so much for taking the time and creating this repo. I really really appreciate it.
Actually I have
connect_infoin the callback, that is the thing thats been the issue.I have been wanting to update the post title, but I don’t have access to that. The issue is I am always getting session as
nil.While this has been a struggle, I have been exploring the Phoenix codebase with Claude, and last nigh I discovered the
error_handler: {__MODULE__, :websocket_error_handler, []}Because WebSocket connections bypass the normal endpoint file flow, I was finally able to make sure the tokens were reaching Phoenix
I have got it down to the fact that it’s 100% a decryption error, but not sure what exactly, will try more and report back.
Since,
Phoenix.Socket.Transport.init_session(session_config)is private, I am emulating it, to see what is the exact issue.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.
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.
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.
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 wasAnd I was using
put_sessionto put cookies I wanted inconnect_infoto 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 doin therouter.exfile, you will findplug :protect_from_forgery, this one line does such a cool thing, if you see the implementationWe are calling
CSRFProtection.call, which if you check the implementation ofdef call(conn, {session_key, mode, allow_hosts}) doplug/lib/plug/csrf_protection.ex at 065976d85f3d079d4f22dde7897c2e7f67c596e0 · elixir-plug/plug · GitHubYou see the missing magic, with
plug :protect_from_forgery, we get for freeunmasked_token = Plug.CSRFProtection.dump_state(), aka the cookie automatically includeput_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