Fetch plug session from cowboy for websocket connection

I have a cowboy server that has had a normal API where I use plug, and I use plug to encode and decode the request’s user session. However, now I need to use websockets as well, and I can’t use plug with the cowboy websockets afaik so I’m kinda stuck on how to retrieve and decode the session.

Have you found a way to solve this? I have a similar situation as you.

I have discontinued that application, but I remember having solved this problem. I believe I found how Plug decrypts the cookie and implemented those methods myself. Here’s the little bit of relevant code I was able to find, even though I can’t promise it will work perfectly. You’ll have to adjust some things for sure. Let me know if you’re able to solve it fully.

defmodule LLServer.Web.Websockets.HubChat do
  @behaviour :cowboy_websocket
  alias Plug.Crypto.{KeyGenerator, MessageEncryptor, MessageVerifier}
  alias Plug.Conn.Cookies

  defp derive(secret_key_base, key, key_opts) do
    secret_key_base
    |> KeyGenerator.generate(key, key_opts)
  end

  defp read_raw_cookie(raw_cookie, opts) do
    signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts)

    case opts do
      %{encryption_salt: nil} ->
        MessageVerifier.verify(raw_cookie, signing_salt)

      %{encryption_salt: _} ->
        encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts)
        MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt)
    end
    |> case do
      :error ->
        nil

      {:ok, result} ->
        (result |> Plug.Crypto.non_executable_binary_to_term([:safe]))["session_token"]
    end
  end

  def init(request, _state) do
    opts = %{
      key_opts: [
        length: 64
      ],
      secret_key_base: Application.fetch_env!(:llserver, :session_secret_key_base),
      encryption_salt: Application.fetch_env!(:llserver, :session_encryption_salt),
      signing_salt: Application.fetch_env!(:llserver, :session_signing_salt)
    }

    cookies = request.headers["cookie"]

    ll_cookie = Cookies.decode(cookies)["_ll_session"]

    session = read_raw_cookie(ll_cookie, opts)
    ...
  end
end

While there are indeed ways to get access to the cookie it needs to be mentioned that phoenix doesn‘t expose it by design. Websockets lack working under the same site policy in browsers, so any website not just your own could establish a websocket connection to your server and the browser would provide the users cookie. Therefore do not use the cookie for authentication. This is knows as cross site websocket hijacking CSWSH.

Phoenix has guides on how to handle authentication with channels and LV. Follow them for a secure approach to authentication over websockets.

1 Like

CSWSH is real, and I believe it’s wise that Phoenix doesn’t make it too easy :+1:

In my specific case, the websocket URL is unique and unguessable (contains long, random token), and I need user ID from the session to verify if the resource is owned by the user. Given one needs to obtain the secret websocket URL first, which is only accessible to the authenticated user, I believe in my case there’s no practical risk.

I’ve settled on the following code to extract user ID from the session in cowboy_websocket handler:

@session_key Keyword.fetch!(Application.compile_env!(:asciinema, :session_opts), :key)
@signing_salt Keyword.fetch!(Application.compile_env!(:asciinema, :session_opts), :signing_salt)

defp user_id_from_session(req) do
  cookies = :cowboy_req.parse_cookies(req)

  with {_, cookie} <- List.keyfind(cookies, @session_key, 0) do
    secret_key_base = Application.fetch_env!(:asciinema, Endpoint)[:secret_key_base]
    conn = %{secret_key_base: secret_key_base}
    opts = Plug.Session.COOKIE.init(signing_salt: @signing_salt)
    {:term, session} = Plug.Session.COOKIE.get(conn, cookie, opts)

    session["user_id"]
  end
end